├── FROSTED
├── bin
├── detect
├── release
└── compile
├── workspaces
├── lib
│ ├── src
│ │ ├── config
│ │ │ └── clients.json
│ │ ├── index.ts
│ │ ├── types
│ │ │ ├── page.ts
│ │ │ ├── category.ts
│ │ │ ├── chapter.ts
│ │ │ ├── item.ts
│ │ │ ├── product.ts
│ │ │ ├── cart.ts
│ │ │ ├── blab.ts
│ │ │ ├── wallet.ts
│ │ │ ├── subscription.ts
│ │ │ ├── index.ts
│ │ │ ├── user.ts
│ │ │ ├── auth.ts
│ │ │ ├── drawing.ts
│ │ │ └── order.ts
│ │ └── util.ts
│ ├── tsconfig.json
│ └── package.json
├── server
│ ├── app.yaml
│ ├── tests
│ │ ├── helpers.ts
│ │ ├── tsconfig.json
│ │ ├── server.tests.ts
│ │ └── db
│ │ │ └── migrations.tests.ts
│ ├── Dockerfile
│ ├── src
│ │ ├── shared
│ │ │ ├── socket
│ │ │ │ ├── handlers
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ ├── types
│ │ │ │ ├── index.ts
│ │ │ │ └── models
│ │ │ │ │ ├── category.ts
│ │ │ │ │ ├── setting.ts
│ │ │ │ │ ├── drawing.ts
│ │ │ │ │ ├── subscription.ts
│ │ │ │ │ ├── product.ts
│ │ │ │ │ ├── wallet.ts
│ │ │ │ │ ├── cart.ts
│ │ │ │ │ ├── item.ts
│ │ │ │ │ ├── order.ts
│ │ │ │ │ └── user.ts
│ │ │ ├── logger.ts
│ │ │ ├── db
│ │ │ │ ├── template.ts
│ │ │ │ └── check.ts
│ │ │ ├── secrets.ts
│ │ │ ├── firebase.ts
│ │ │ ├── auth
│ │ │ │ └── auth0.ts
│ │ │ └── trace.ts
│ │ ├── routes
│ │ │ ├── index.ts
│ │ │ ├── main
│ │ │ │ └── index.ts
│ │ │ ├── shop
│ │ │ │ ├── index.ts
│ │ │ │ └── paypal.ts
│ │ │ ├── stripe
│ │ │ │ └── index.ts
│ │ │ └── swagger.yaml
│ │ ├── config
│ │ │ └── app.json
│ │ ├── index.ts
│ │ └── app.ts
│ ├── .eslintrc.json
│ ├── jest.config.js
│ └── tsconfig.json
├── client
│ ├── .firebaserc
│ ├── public
│ │ ├── robots.txt
│ │ ├── favicon.ico
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ ├── index.html
│ │ └── 404.html
│ ├── src
│ │ ├── features
│ │ │ ├── app
│ │ │ │ ├── index.ts
│ │ │ │ ├── types.ts
│ │ │ │ ├── App.test.tsx
│ │ │ │ ├── LanguageProvider.tsx
│ │ │ │ ├── ConfigProvider.tsx
│ │ │ │ ├── App.tsx
│ │ │ │ ├── slice.ts
│ │ │ │ └── SocketListener.tsx
│ │ │ ├── languages
│ │ │ │ ├── es.json
│ │ │ │ ├── en.json
│ │ │ │ └── extract.json
│ │ │ ├── ui
│ │ │ │ ├── Spacer.tsx
│ │ │ │ ├── Consent.tsx
│ │ │ │ ├── BackToTop.tsx
│ │ │ │ ├── StyledFab.tsx
│ │ │ │ ├── Card
│ │ │ │ │ ├── Skeleton
│ │ │ │ │ │ ├── ImagePlaceholder.tsx
│ │ │ │ │ │ ├── EarningCard.tsx
│ │ │ │ │ │ ├── TotalIncomeCard.tsx
│ │ │ │ │ │ ├── TotalGrowthBarChart.tsx
│ │ │ │ │ │ └── ProductPlaceholder.tsx
│ │ │ │ │ ├── SubCard.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── LoadingLine.tsx
│ │ │ │ ├── BlurBackdrop.tsx
│ │ │ │ ├── Theme
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Dialogs.tsx
│ │ │ │ ├── TabPanel.tsx
│ │ │ │ ├── CircularProgressWithLabel.tsx
│ │ │ │ ├── PasswordField.tsx
│ │ │ │ ├── MainLayout.tsx
│ │ │ │ ├── Drawer.tsx
│ │ │ │ ├── Routing.tsx
│ │ │ │ ├── Footer.tsx
│ │ │ │ ├── StyledSlider.tsx
│ │ │ │ ├── AlertDialog.tsx
│ │ │ │ ├── Notifications.tsx
│ │ │ │ └── extended
│ │ │ │ │ ├── Avatar.tsx
│ │ │ │ │ └── AnimateButton.tsx
│ │ │ ├── admin
│ │ │ │ ├── Vertical.tsx
│ │ │ │ ├── Active.tsx
│ │ │ │ ├── Users
│ │ │ │ │ ├── DialogWrap.tsx
│ │ │ │ │ └── UserOrders.tsx
│ │ │ │ ├── Orders.tsx
│ │ │ │ ├── thunks.ts
│ │ │ │ ├── Subscriptions
│ │ │ │ │ └── hooks.ts
│ │ │ │ ├── slice.ts
│ │ │ │ ├── Dashboard
│ │ │ │ │ ├── images
│ │ │ │ │ │ ├── earning.svg
│ │ │ │ │ │ └── social-google.svg
│ │ │ │ │ ├── chart-data
│ │ │ │ │ │ ├── bajaj-area-chart.ts
│ │ │ │ │ │ ├── total-order-month-line-chart.ts
│ │ │ │ │ │ ├── total-order-year-line-chart.ts
│ │ │ │ │ │ └── total-growth-bar-chart.ts
│ │ │ │ │ ├── BajajAreaChartCard.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Footer.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── MenuItem.tsx
│ │ │ ├── pages
│ │ │ │ ├── Terms1.tsx
│ │ │ │ ├── 404.tsx
│ │ │ │ └── Maintenance.tsx
│ │ │ ├── shop
│ │ │ │ ├── images
│ │ │ │ │ └── stripe.png
│ │ │ │ ├── FakeCheckout.tsx
│ │ │ │ ├── Receipt.tsx
│ │ │ │ ├── slice.ts
│ │ │ │ ├── OrderAddress.tsx
│ │ │ │ └── Payment.tsx
│ │ │ ├── profile
│ │ │ │ ├── AuthProviders.tsx
│ │ │ │ ├── AuthCheck.tsx
│ │ │ │ ├── Orders.tsx
│ │ │ │ └── Callback.tsx
│ │ │ ├── canvas
│ │ │ │ ├── LoadingCanvas.tsx
│ │ │ │ ├── Player.tsx
│ │ │ │ ├── LineSize.tsx
│ │ │ │ ├── NameEdit.tsx
│ │ │ │ ├── Toolbar.tsx
│ │ │ │ ├── slice.ts
│ │ │ │ ├── Color.tsx
│ │ │ │ └── worker.ts
│ │ │ └── home
│ │ │ │ ├── images
│ │ │ │ ├── github.svg
│ │ │ │ ├── redux.svg
│ │ │ │ ├── ts.svg
│ │ │ │ └── nodejs.svg
│ │ │ │ ├── index.tsx
│ │ │ │ ├── Subscribe.tsx
│ │ │ │ ├── HeroSection.tsx
│ │ │ │ └── Logos.tsx
│ │ ├── shared
│ │ │ ├── constant.ts
│ │ │ ├── debouncer.ts
│ │ │ ├── selectFile.ts
│ │ │ ├── config.ts
│ │ │ ├── loadConfig.ts
│ │ │ ├── store.ts
│ │ │ └── testing.tsx
│ │ ├── index.tsx
│ │ ├── reportWebVitals.ts
│ │ ├── setupTests.ts
│ │ └── react-app-env.d.ts
│ ├── tools
│ │ ├── style-mock.js
│ │ ├── webpack
│ │ │ └── persistentCache
│ │ │ │ └── createEnvironmentHash.js
│ │ ├── jest
│ │ │ ├── cssTransform.js
│ │ │ ├── babelTransform.js
│ │ │ └── fileTransform.js
│ │ ├── react-intl-sync.js
│ │ └── getHttpsConfig.js
│ ├── .env.example
│ ├── firebase.json
│ ├── tsconfig.json
│ ├── .eslintrc.json
│ ├── jest.config.js
│ ├── scripts
│ │ └── test.js
│ └── .gitignore
├── tsconfig.shared.json
└── function-gkill
│ ├── tsconfig.json
│ └── package.json
├── Procfile
├── docs
├── images
│ ├── 4Pane.png
│ └── lighthouse.png
├── dev.md
├── README.md
└── deploy.md
├── .husky
├── pre-commit
└── pre-push
├── .editorconfig
├── jest.config.js
├── prettier.config.js
├── app.json
├── docker-compose.yml
├── tools
└── createEnvironmentHash.js
├── .yarnrc.yml
├── .github
└── workflows
│ ├── tests.yml
│ ├── firebase-hosting-pull-request.yml
│ ├── firebase-hosting-live.yml
│ ├── client-deploy-ghpages.yml
│ └── deploy-google-server.yml
├── amplify.yml
├── .vscode
├── settings.json
├── launch.json
└── tasks.json
├── .eslintrc.json
├── .gitignore
├── README.md
└── package.json
/FROSTED:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bin/detect:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bin/release:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/workspaces/lib/src/config/clients.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/workspaces/server/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: nodejs18
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: PORT=$PORT node workspaces/server/dist/index.js
2 |
--------------------------------------------------------------------------------
/workspaces/lib/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './util'
3 |
--------------------------------------------------------------------------------
/docs/images/4Pane.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ruyd/fullstack-monorepo/HEAD/docs/images/4Pane.png
--------------------------------------------------------------------------------
/workspaces/client/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "drawspace-6c652"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | yarn precommit
5 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/images/lighthouse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ruyd/fullstack-monorepo/HEAD/docs/images/lighthouse.png
--------------------------------------------------------------------------------
/workspaces/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/app/index.ts:
--------------------------------------------------------------------------------
1 | export * from './slice'
2 | export * from './thunks'
3 | export * from './types'
4 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/languages/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "home.button": "Pruebalo",
3 | "home.title": "Bazaar de Arte"
4 | }
--------------------------------------------------------------------------------
/workspaces/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ruyd/fullstack-monorepo/HEAD/workspaces/client/public/favicon.ico
--------------------------------------------------------------------------------
/workspaces/client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ruyd/fullstack-monorepo/HEAD/workspaces/client/public/logo192.png
--------------------------------------------------------------------------------
/workspaces/client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ruyd/fullstack-monorepo/HEAD/workspaces/client/public/logo512.png
--------------------------------------------------------------------------------
/workspaces/client/tools/style-mock.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | process() {
3 | return 'module.exports = {};'
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Spacer.tsx:
--------------------------------------------------------------------------------
1 | export default function Spacer() {
2 | return
3 | }
4 |
--------------------------------------------------------------------------------
/workspaces/server/tests/helpers.ts:
--------------------------------------------------------------------------------
1 | jest.mock('../src/shared/logger')
2 |
3 | export function beforeAllHook() {
4 | // ...
5 | }
6 |
--------------------------------------------------------------------------------
/workspaces/client/.env.example:
--------------------------------------------------------------------------------
1 | BACKEND=http://localhost:3001
2 | GOOGLE_CLIENT_ID=
3 | AUTH_REDIRECT_URL=http://localhost:3000/callback
4 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/languages/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "home.button": "Take it for a spin",
3 | "home.title": "Drawings Marketplace"
4 | }
--------------------------------------------------------------------------------
/workspaces/client/src/shared/constant.ts:
--------------------------------------------------------------------------------
1 | export const gridSpacing = 3
2 | export const drawerWidth = 260
3 | export const appDrawerWidth = 320
4 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Vertical.tsx:
--------------------------------------------------------------------------------
1 | export default function VerticalPlayer() {
2 | return
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | insert_final_newline = true
9 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | module.exports = {
3 | projects: ['/workspaces/*/jest.config.js'],
4 | }
5 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/pages/Terms1.tsx:
--------------------------------------------------------------------------------
1 | export default function Terms() {
2 | return (
3 |
4 |
Terms
5 |
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/shop/images/stripe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ruyd/fullstack-monorepo/HEAD/workspaces/client/src/features/shop/images/stripe.png
--------------------------------------------------------------------------------
/workspaces/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18
2 | WORKDIR /app
3 | ADD ./dist .
4 | RUN npm install --production
5 | EXPOSE 80 8080 443
6 | CMD ["node", "index.js"]
7 |
8 |
--------------------------------------------------------------------------------
/workspaces/server/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": true
5 | },
6 | "include": ["**/*.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Active.tsx:
--------------------------------------------------------------------------------
1 | export default function Active(): JSX.Element {
2 | return (
3 |
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 100,
3 | singleQuote: true,
4 | trailingComma: 'none',
5 | arrowParens: 'avoid',
6 | semi: false,
7 | tabWidth: 2,
8 | }
9 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Consent.tsx:
--------------------------------------------------------------------------------
1 | export default function Consent(): JSX.Element {
2 | return (
3 |
4 |
Consent Banner
5 |
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildpacks": [
3 | {
4 | "url": "heroku/nodejs"
5 | },
6 | {
7 | "url": "https://github.com/heroku/heroku-buildpack-inline.git"
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | database:
3 | image: postgres:14.5
4 | ports:
5 | - 5432:5432
6 | environment:
7 | POSTGRES_USER: postgres
8 | POSTGRES_PASSWORD: postgrespass
9 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/BackToTop.tsx:
--------------------------------------------------------------------------------
1 | import Fab from '@mui/material/Fab'
2 |
3 | export default function FabBackToTop() {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/page.ts:
--------------------------------------------------------------------------------
1 | import { Blab } from './blab'
2 |
3 | export interface Page {
4 | pageId?: string
5 | path?: string
6 | title?: string
7 | roles?: string[]
8 | blabs?: Blab[]
9 | }
10 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/profile/AuthProviders.tsx:
--------------------------------------------------------------------------------
1 | import { GoogleOneTap } from '../profile/GoogleOneTap'
2 | export default function AuthProviderInjections() {
3 | return (
4 | <>
5 |
6 | >
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/category.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Item } from '.'
2 |
3 | export interface Category extends Entity {
4 | categoryId?: string
5 | title?: string
6 | imageUrl: string
7 | urlName?: string
8 | items?: Item[]
9 | }
10 |
--------------------------------------------------------------------------------
/tools/createEnvironmentHash.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { createHash } = require('crypto');
3 |
4 | module.exports = env => {
5 | const hash = createHash('md5');
6 | hash.update(JSON.stringify(env));
7 |
8 | return hash.digest('hex');
9 | };
10 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Users/DialogWrap.tsx:
--------------------------------------------------------------------------------
1 | export default function DialogWrap({ children }: { children: React.ReactNode }): JSX.Element {
2 | return (
3 |
4 |
DialogWrap
5 | {children}
6 |
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/chapter.ts:
--------------------------------------------------------------------------------
1 | import { Blab } from './blab'
2 |
3 | export interface Chapter {
4 | chapterId?: string
5 | titleId?: number
6 | chapterNumber?: number
7 | title?: string
8 | chapter?: string
9 | blabs?: Blab[]
10 | }
11 |
--------------------------------------------------------------------------------
/workspaces/lib/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.shared.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "baseUrl": "."
6 | },
7 | "include": ["src", ".eslintrc.js", "package.json"],
8 | "exclude": ["node_modules", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/workspaces/client/tools/webpack/persistentCache/createEnvironmentHash.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const { createHash } = require('crypto')
3 |
4 | module.exports = env => {
5 | const hash = createHash('md5')
6 | hash.update(JSON.stringify(env))
7 |
8 | return hash.digest('hex')
9 | }
10 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/socket/handlers/index.ts:
--------------------------------------------------------------------------------
1 | import { SocketHandler } from '..'
2 |
3 | export const userHandler: SocketHandler = (io, socket) => {
4 | socket.on('user:message', data => {
5 | console.log('user', data)
6 | })
7 | }
8 |
9 | export default [userHandler]
10 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/languages/extract.json:
--------------------------------------------------------------------------------
1 | {
2 | "home.button": {
3 | "defaultMessage": "Take it for a spin",
4 | "description": "Hero Button Caption"
5 | },
6 | "home.title": {
7 | "defaultMessage": "Drawings Marketplace",
8 | "description": "Hero Title"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | plugins:
4 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
5 | spec: "@yarnpkg/plugin-workspace-tools"
6 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
7 | spec: "@yarnpkg/plugin-typescript"
8 |
9 | yarnPath: .yarn/releases/yarn-3.3.0.cjs
10 |
--------------------------------------------------------------------------------
/workspaces/server/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "@typescript-eslint/no-unused-vars": "error",
4 | "no-console": "warn"
5 | },
6 | "settings": {
7 | "import/resolver": {
8 | "node": true,
9 | "typescript": {
10 | "project": "./tsconfig.json"
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/app/types.ts:
--------------------------------------------------------------------------------
1 | export enum NotificationSeverity {
2 | info = 'info',
3 | success = 'success',
4 | warning = 'warning',
5 | error = 'error',
6 | }
7 | export interface AppNotification {
8 | id: string
9 | message: string
10 | severity?: NotificationSeverity
11 | closed?: boolean
12 | }
13 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Orders.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Orders needs special view, we want to see oneline status as well as certain actions
4 | * maybe we can enrich generic editor via props, think
5 | */
6 | export default function Orders() {
7 | return (
8 |
9 |
Orders
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/workspaces/server/src/routes/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import main from './main'
3 | import profile from './profile'
4 | import shop from './shop'
5 | import stripe from './stripe'
6 |
7 | const router = express.Router()
8 | router.use(main)
9 | router.use(profile)
10 | router.use(shop)
11 | router.use(stripe)
12 | export default router
13 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/StyledFab.tsx:
--------------------------------------------------------------------------------
1 | import Fab from '@mui/material/Fab'
2 | import { styled } from '@mui/material/styles'
3 |
4 | export const StyledFab = styled(Fab)(({ theme }) => ({
5 | [theme.breakpoints.up('md')]: {},
6 | position: 'absolute',
7 | right: '1em',
8 | bottom: '1em',
9 | zIndex: 1
10 | }))
11 |
12 | export default StyledFab
13 |
--------------------------------------------------------------------------------
/workspaces/client/src/shared/debouncer.ts:
--------------------------------------------------------------------------------
1 | const cache: Record = {}
2 | export const debouncer = (key: string, callback: () => void, delay = 1000) => {
3 | if (cache[key]) {
4 | window.clearTimeout(cache[key])
5 | }
6 | const timer = window.setTimeout(() => callback(), delay)
7 | cache[key] = timer
8 | }
9 |
10 | export default debouncer
11 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | tests:
8 | runs-on: 'ubuntu-latest'
9 | steps:
10 | - uses: 'actions/checkout@v3'
11 |
12 | - uses: actions/checkout@v3
13 |
14 | - name: Install
15 | run: yarn install --immutable
16 |
17 | - name: Tests
18 |
19 | run: yarn test
20 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Card/Skeleton/ImagePlaceholder.tsx:
--------------------------------------------------------------------------------
1 | // material-ui
2 | import Skeleton from '@mui/material/Skeleton';
3 |
4 | // ==============================|| SKELETON IMAGE CARD ||============================== //
5 |
6 | const ImagePlaceholder = ({ ...others }) => ;
7 |
8 | export default ImagePlaceholder;
9 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import Container from '@mui/material/Container'
2 | import image from './images/404.svg'
3 | import Typography from '@mui/material/Typography'
4 |
5 | export default function Error404() {
6 | return (
7 |
8 |
9 | Oops!
10 |
11 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/workspaces/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './styles/index.scss'
4 | import App from './features/app/App'
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
7 | const container = document.getElementById('root')!
8 | const root = createRoot(container)
9 | root.render(
10 |
11 |
12 |
13 | )
14 |
--------------------------------------------------------------------------------
/amplify.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | applications:
3 | - frontend:
4 | phases:
5 | preBuild:
6 | commands:
7 | - yarn install
8 | build:
9 | commands:
10 | - yarn workspace client build
11 | artifacts:
12 | baseDirectory: build
13 | files:
14 | - '**/*'
15 | cache:
16 | paths:
17 | - node_modules/**/*
18 | appRoot: workspaces/client
19 |
--------------------------------------------------------------------------------
/workspaces/server/src/routes/main/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import { checkFirebase, gallery, sendClientConfigSettings, start } from './controller'
3 |
4 | const router = express.Router()
5 |
6 | router.get(['/gallery', '/gallery/:userId'], gallery)
7 |
8 | router.get('/config', sendClientConfigSettings)
9 |
10 | router.post('/start', start)
11 |
12 | router.post('/firebase/check', checkFirebase)
13 |
14 | export default router
15 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/LoadingLine.tsx:
--------------------------------------------------------------------------------
1 | import LinearProgress from '@mui/material/LinearProgress'
2 | import { useAppSelector } from '../../shared/store'
3 |
4 | export default function LoadingLine() {
5 | const loading = useAppSelector(store => store.app.loading)
6 | return (
7 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/item.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from '.'
2 | import { Blab } from './blab'
3 | import { Chapter } from './chapter'
4 |
5 | export interface Item extends Entity {
6 | itemId?: string
7 | title?: string
8 | urlName?: string
9 | subscriptions?: string[]
10 | tokens?: number
11 | price?: number
12 | currency?: string
13 | inventory?: number
14 | blabs?: Blab[]
15 | paywall?: boolean
16 | tags?: string[]
17 | chapters?: Chapter[]
18 | }
19 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/product.ts:
--------------------------------------------------------------------------------
1 | export interface Price {
2 | id: string
3 | amount: number
4 | currency: string
5 | interval?: string
6 | intervalCount?: number
7 | freeTrialDays?: number
8 | divide_by?: number
9 | }
10 |
11 | export interface Product {
12 | productId: string
13 | title: string
14 | description: string
15 | imageUrl?: string
16 | images?: string[]
17 | keywords?: string
18 | prices?: Price[]
19 | shippable?: boolean
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsserver.experimental.enableProjectDiagnostics": true,
3 | "eslint.useESLintClass": true,
4 | "files.exclude": {
5 | "amplify/.config": true,
6 | "amplify/**/*-parameters.json": true,
7 | "amplify/**/amplify.state": true,
8 | "amplify/**/transform.conf.json": true,
9 | "amplify/#current-cloud-backend": true,
10 | "amplify/backend/amplify-meta.json": true,
11 | "amplify/backend/awscloudformation": true
12 | }
13 | }
--------------------------------------------------------------------------------
/workspaces/tsconfig.shared.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "incremental": true,
4 | "composite": true,
5 | "declaration": true,
6 | "strict": true,
7 | "target": "ES2021",
8 | "module": "commonjs",
9 | "lib": ["ES2021"],
10 | "moduleResolution": "node",
11 | "sourceMap": true,
12 | "preserveConstEnums": true,
13 | "resolveJsonModule": true,
14 | "esModuleInterop": true,
15 | "skipLibCheck": true,
16 | "outDir": "dist"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/canvas/LoadingCanvas.tsx:
--------------------------------------------------------------------------------
1 | import CircularProgress from '@mui/material/CircularProgress'
2 | import { useAppSelector } from '../../shared/store'
3 |
4 | export default function LoadingCanvas() {
5 | const loading = useAppSelector(store => store.canvas.loading)
6 | return (
7 | <>
8 |
12 | >
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/profile/AuthCheck.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useAppSelector } from '../../shared/store'
3 | import Login from './Login'
4 |
5 | export default function AuthCheck({
6 | children,
7 | secure,
8 | }: {
9 | children: React.ReactNode
10 | secure?: boolean
11 | }) {
12 | const token = useAppSelector(state => state.app.token)
13 | const denied = secure && !token
14 | if (denied) {
15 | return
16 | }
17 | return children as JSX.Element
18 | }
19 |
--------------------------------------------------------------------------------
/workspaces/client/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals'
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry)
7 | getFID(onPerfEntry)
8 | getFCP(onPerfEntry)
9 | getLCP(onPerfEntry)
10 | getTTFB(onPerfEntry)
11 | })
12 | }
13 | }
14 |
15 | export default reportWebVitals
16 |
--------------------------------------------------------------------------------
/workspaces/server/src/routes/shop/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import { addSubscriptionToCart, checkout } from './controller'
3 | import { capturePaymentHandler, createOrderHandler } from './paypal'
4 |
5 | const router = express.Router()
6 |
7 | router.post('/shop/checkout', checkout)
8 |
9 | router.post('/shop/subscribe', addSubscriptionToCart)
10 |
11 | router.post('/paypal/order', createOrderHandler)
12 |
13 | router.post('/paypal/orders/:orderID/capture', capturePaymentHandler)
14 |
15 | export default router
16 |
--------------------------------------------------------------------------------
/workspaces/client/tools/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 |
4 | // This is a custom Jest transformer turning style imports into empty objects.
5 | // http://facebook.github.io/jest/docs/en/webpack.html
6 |
7 | module.exports = {
8 | process(sourceText, sourcePath, options) {
9 | return {
10 | code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`,
11 | }
12 | },
13 | getCacheKey() {
14 | // The output is always the same.
15 | return 'cssTransform'
16 | },
17 | }
18 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/BlurBackdrop.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@mui/material/styles/styled'
2 |
3 | const StyledDiv = styled('div')`
4 | backdrop-filter: blur(6px);
5 | position: absolute;
6 | right: 0;
7 | left: 0;
8 | `
9 | export function BlurBackdrop({
10 | height,
11 | bottom
12 | }: {
13 | height?: string | number
14 | bottom?: string | number
15 | }): JSX.Element {
16 | return (
17 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './models/drawing'
2 | export * from './models/user'
3 | export * from './models/setting'
4 | export * from './models/cart'
5 | export * from './models/order'
6 | export * from './models/subscription'
7 | export * from './models/category'
8 | export * from './models/item'
9 |
10 | import express from 'express'
11 | import { EntityConfig } from '../db'
12 | import { AppAccessToken } from '@lib'
13 |
14 | export type EnrichedRequest = express.Request & {
15 | auth: AppAccessToken
16 | config?: EntityConfig
17 | }
18 |
--------------------------------------------------------------------------------
/workspaces/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Drawspace",
3 | "name": "Drawspace",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/thunks.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* eslint-disable @typescript-eslint/no-unused-vars */
3 | import { AnyAction, createAsyncThunk, ThunkDispatch } from '@reduxjs/toolkit'
4 | import { GridPatchProps, PagedResult } from '@lib'
5 | import { get } from '../app'
6 | import { patch } from './slice'
7 |
8 | export const loadDataAsync = createAsyncThunk(
9 | 'admin/data/read',
10 | async (name: string, { dispatch, getState }) => {
11 | const response = await get(`${name}`)
12 | dispatch(patch({ data: { [name]: { ...response.data } } }))
13 | },
14 | )
15 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/cart.ts:
--------------------------------------------------------------------------------
1 | import { Drawing } from './drawing'
2 | import { Price, Product } from './product'
3 |
4 | export const CartType = {
5 | PRODUCT: 'product',
6 | SUBSCRIPTION: 'subscription',
7 | DRAWING: 'drawing',
8 | TOKENS: 'tokens'
9 | } as const
10 |
11 | export type CartType = typeof CartType[keyof typeof CartType]
12 |
13 | export interface Cart {
14 | cartId: string
15 | userId: string
16 | quantity: number
17 | cartType?: CartType
18 | drawingId?: string
19 | productId?: string
20 | priceId?: string
21 | drawing?: Drawing
22 | product?: Partial
23 | }
24 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Theme/index.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from '@mui/material/styles'
2 | import React from 'react'
3 | import { useAppSelector } from '../../../shared/store'
4 | import { getTheme } from './getTheme'
5 |
6 | export default function ThemeSwitch({ children }: React.PropsWithChildren): JSX.Element {
7 | const darkTheme = useAppSelector(store => store.app.darkMode)
8 | const state = useAppSelector(store => store.app.ui)
9 | const theme = React.useMemo(() => getTheme(darkTheme, state), [darkTheme, state])
10 | return {children}
11 | }
12 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/types/models/category.ts:
--------------------------------------------------------------------------------
1 | import { Category } from '@lib'
2 | import { DataTypes } from 'sequelize'
3 | import { EntityDefinition } from '../../db'
4 | import { addModel } from '../../db/util'
5 |
6 | export const CategoryDefinition: EntityDefinition = {
7 | categoryId: {
8 | type: DataTypes.STRING,
9 | primaryKey: true
10 | },
11 | title: {
12 | type: DataTypes.STRING
13 | },
14 | imageUrl: {
15 | type: DataTypes.STRING
16 | }
17 | }
18 |
19 | export const CategoryModel = addModel({
20 | name: 'category',
21 | attributes: CategoryDefinition
22 | })
23 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/blab.ts:
--------------------------------------------------------------------------------
1 | export const BlabTypes = {
2 | Text: 'text',
3 | Image: 'image',
4 | Video: 'video',
5 | Audio: 'audio',
6 | File: 'file'
7 | } as const
8 |
9 | export type BlabType = typeof BlabTypes[keyof typeof BlabTypes]
10 |
11 | export interface Blab {
12 | BlabId?: string
13 | BlabType?: BlabType
14 | Blab?: string
15 | BlabName?: string
16 | BlabDescription?: string
17 | BlabUrl?: string
18 | BlabSize?: number
19 | BlabWidth?: number
20 | BlabHeight?: number
21 | BlabDuration?: number
22 | BlabEncoding?: string
23 | BlabMimeType?: string
24 | BlabExtension?: string
25 | }
26 |
--------------------------------------------------------------------------------
/workspaces/server/src/routes/stripe/index.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import {
3 | stripeCreatePaymentIntent,
4 | stripeCreateVerifyIdentitySession,
5 | stripeWebHook,
6 | syncProductsHandler
7 | } from '../stripe/controller'
8 |
9 | const router = express.Router()
10 |
11 | router.post('/stripe/payment/intent', stripeCreatePaymentIntent)
12 |
13 | router.post('/stripe/identity/start', stripeCreateVerifyIdentitySession)
14 |
15 | router.post('/stripe/webhook', express.raw({ type: 'application/json' }), stripeWebHook)
16 |
17 | router.get('/stripe/products/sync', syncProductsHandler)
18 |
19 | export default router
20 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | declare HAS_CLIENT_CHANGE=$(git show --name-only -r --stat --oneline HEAD^^..HEAD | grep 'workspaces/client')
5 | declare HAS_SERVER_CHANGE=$(git show --name-only -r --stat --oneline HEAD^^..HEAD | grep 'workspaces/server')
6 |
7 | if [ -n "$HAS_CLIENT_CHANGE" ]; then
8 | echo "Client changed, testing client build"
9 | yarn jest --selectProjects client --maxWorkers=50%
10 | fi
11 |
12 | if [ -n "$HAS_SERVER_CHANGE" ]; then
13 | echo "Server changed, testing server build"
14 | yarn docker && yarn jest --selectProjects server --runInBand --bail
15 | fi
16 |
17 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/wallet.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from '.'
2 |
3 | export interface WalletTransaction extends Entity {
4 | transactionId?: string
5 | userId?: string
6 | amount?: number
7 | orderId?: string
8 | }
9 |
10 | export const WalletStatus = {
11 | Active: 'active',
12 | Flagged: 'flagged',
13 | Frozen: 'frozen',
14 | Closed: 'closed'
15 | } as const
16 |
17 | export type WalletStatus = typeof WalletStatus[keyof typeof WalletStatus]
18 |
19 | export interface Wallet extends Entity {
20 | walletId?: string
21 | balance?: number
22 | currency?: string
23 | status?: WalletStatus
24 | transactions?: WalletTransaction[]
25 | }
26 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/types/models/setting.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from '@lib'
2 | import { DataTypes } from 'sequelize'
3 | import { sendConfig } from '../../../shared/socket'
4 | import { addModel } from '../../../shared/db/util'
5 |
6 | export const SettingModel = addModel({
7 | name: 'setting',
8 | attributes: {
9 | name: {
10 | primaryKey: true,
11 | type: DataTypes.STRING
12 | },
13 | data: {
14 | type: DataTypes.JSONB
15 | }
16 | },
17 | joins: [],
18 | roles: ['admin'],
19 | publicRead: false,
20 | publicWrite: false,
21 | onChanges: sendConfig
22 | })
23 |
24 | export default SettingModel
25 |
--------------------------------------------------------------------------------
/workspaces/server/jest.config.js:
--------------------------------------------------------------------------------
1 | const { pathsToModuleNameMapper } = require('ts-jest')
2 | const { compilerOptions } = require('./tsconfig.json')
3 | const paths = compilerOptions.paths || {}
4 | const rootPath = compilerOptions.baseUrl || '.'
5 | /** @type {import('ts-jest').JestConfigWithTsJest} */
6 | module.exports = {
7 | preset: 'ts-jest',
8 | displayName: 'server',
9 | testEnvironment: 'node',
10 | modulePathIgnorePatterns: ['/build/', '/dist/'],
11 | roots: [''],
12 | testMatch: ['/tests/**/*.tests.ts'],
13 | modulePaths: [rootPath],
14 | moduleNameMapper: pathsToModuleNameMapper(paths, { prefix: '/' }),
15 | }
16 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/logger.ts:
--------------------------------------------------------------------------------
1 | import winston from 'winston'
2 |
3 | const format = winston.format.combine(winston.format.timestamp(), winston.format.simple())
4 | const logger = winston.createLogger({
5 | level: 'info',
6 | transports: [
7 | new winston.transports.Console({
8 | format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
9 | }),
10 | new winston.transports.File({
11 | filename: '_error.log',
12 | level: 'error',
13 | format,
14 | }),
15 | new winston.transports.File({
16 | filename: '_trace.log',
17 | format,
18 | }),
19 | ],
20 | })
21 |
22 | export default logger
23 |
--------------------------------------------------------------------------------
/workspaces/client/tools/jest/babelTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const babelJest = require('babel-jest').default
4 |
5 | const hasJsxRuntime = (() => {
6 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
7 | return false
8 | }
9 |
10 | try {
11 | require.resolve('react/jsx-runtime')
12 | return true
13 | } catch (e) {
14 | return false
15 | }
16 | })()
17 |
18 | module.exports = babelJest.createTransformer({
19 | presets: [
20 | [
21 | require.resolve('babel-preset-react-app'),
22 | {
23 | runtime: hasJsxRuntime ? 'automatic' : 'classic',
24 | },
25 | ],
26 | ],
27 | babelrc: false,
28 | configFile: false,
29 | })
30 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Dialogs.tsx:
--------------------------------------------------------------------------------
1 | import { routes } from '../../shared/routes'
2 | import OnboardingDialog from '../profile/OnboardingDialog'
3 | const dialogs = routes.filter(route => route.dialog && !route.dialog?.includes('onboard'))
4 |
5 | /**
6 | * Multi component dialogs that return empty if not open
7 | * Dev Note: Single component level dialogs -> inside component
8 | * @returns
9 | */
10 | export default function Dialogs() {
11 | return (
12 | <>
13 | {dialogs.map(dialog => {
14 | const DialogComponent = dialog.component
15 | return
16 | })}
17 |
18 | >
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/workspaces/function-gkill/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowUnreachableCode": false,
4 | "allowUnusedLabels": false,
5 | "declaration": false,
6 | "forceConsistentCasingInFileNames": true,
7 | "lib": ["es2018"],
8 | "module": "commonjs",
9 | "noEmitOnError": true,
10 | "esModuleInterop": true,
11 | "resolveJsonModule": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "noImplicitReturns": true,
14 | "sourceMap": true,
15 | "strict": true,
16 | "allowJs": false,
17 | "target": "es2018",
18 | "rootDir": ".",
19 | "outDir": "dist"
20 | },
21 | "include": ["src", "package.json"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/workspaces/server/tests/server.tests.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest'
2 | import createBackendApp from '../src/app'
3 | import { getRoutesFromApp } from '../src/shared/server'
4 | import { beforeAllHook } from './helpers'
5 |
6 | beforeAll(() => beforeAllHook())
7 | describe('server route checks', () => {
8 | const app = createBackendApp({ checks: false, trace: false })
9 | const routes = getRoutesFromApp(app)
10 |
11 | test('should have at least one route', () => {
12 | expect(routes.length).toBeGreaterThan(0)
13 | })
14 | test('should return a 200 status code', async () => {
15 | const response = await request(app).get('/')
16 | expect(response.status).toBe(200)
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/workspaces/client/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "headers": [
10 | {
11 | "source": "**",
12 | "headers": [
13 | {
14 | "key": "Access-Control-Allow-Origin",
15 | "value": "*"
16 | },
17 | {
18 | "key": "Access-Control-Allow-Headers",
19 | "value": "Access-Control-Allow-Origin"
20 | }
21 | ]
22 | }
23 | ],
24 | "rewrites": [
25 | {
26 | "source": "**",
27 | "destination": "/index.html"
28 | }
29 | ]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Subscriptions/hooks.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { useQuery } from 'react-query'
3 | import config from 'src/shared/config'
4 |
5 | export function useProducts() {
6 | const stripeProducts = useQuery(
7 | 'stripe.products',
8 | async () => {
9 | await axios.get('https://api.stripe.com/v1/products', {
10 | auth: {
11 | username: config.settings.system?.paymentMethods?.stripe?.publishableKey || '',
12 | password: '',
13 | },
14 | })
15 | },
16 | {
17 | enabled: false,
18 | },
19 | )
20 | // eslint-disable-next-line no-console
21 | console.log(stripeProducts)
22 | return stripeProducts
23 | }
24 |
--------------------------------------------------------------------------------
/workspaces/client/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect'
6 |
7 | Object.defineProperty(window, 'matchMedia', {
8 | writable: true,
9 | value: jest.fn().mockImplementation(query => ({
10 | matches: false,
11 | media: query,
12 | onchange: null,
13 | addListener: jest.fn(), // Deprecated
14 | removeListener: jest.fn(), // Deprecated
15 | addEventListener: jest.fn(),
16 | removeEventListener: jest.fn(),
17 | dispatchEvent: jest.fn(),
18 | })),
19 | })
20 |
--------------------------------------------------------------------------------
/workspaces/server/tests/db/migrations.tests.ts:
--------------------------------------------------------------------------------
1 | import { checkMigrations } from '../../src/shared/db/migrator'
2 | import { beforeAllHook } from 'tests/helpers'
3 | import createBackendApp from '../../src/app'
4 | import { Connection } from '../../src/shared/db'
5 |
6 | beforeAll(() => beforeAllHook())
7 | afterAll(() => {
8 | Connection.db.close()
9 | })
10 | describe('database migrations', () => {
11 | const app = createBackendApp({ checks: true, trace: false })
12 | test('startup', async () => {
13 | const checks = await app.onStartupCompletePromise
14 | expect(checks[0]).toBeTruthy()
15 | })
16 |
17 | test('migrate', async () => {
18 | const result = await checkMigrations()
19 | expect(result).toBeTruthy()
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/workspaces/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2021",
4 | "module": "commonjs",
5 | "lib": ["ES2021"],
6 | "removeComments": false,
7 | "moduleResolution": "node",
8 | "esModuleInterop": true,
9 | "resolveJsonModule": true,
10 | "strict": true,
11 | "sourceMap": true,
12 | "skipLibCheck": true,
13 | "baseUrl": ".",
14 | "noEmit": true,
15 | "outDir": "build",
16 | "paths": {
17 | "@lib": ["../lib/src"]
18 | }
19 | },
20 | "include": ["src", ".eslintrc.js", "package.json", "src/config/*.json"],
21 | "exclude": ["dist", "node_modules", "../../node_modules"],
22 | "references": [
23 | {
24 | "path": "../lib/tsconfig.json"
25 | }
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/db/template.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize } from 'sequelize'
2 |
3 | // Sync() will create any new objects but will not alter existing ones
4 | // Migrations used for changes to existing objects to prevent data loss
5 |
6 | export const up = async ({ context }: { context: Sequelize }) => {
7 | const qi = context.getQueryInterface()
8 | const transaction = await context.transaction()
9 | try {
10 | // changes
11 | qi.changeColumn
12 | await transaction.commit()
13 | } catch (err) {
14 | await transaction.rollback()
15 | throw err
16 | }
17 | }
18 |
19 | export const down = async ({ context }: { context: Sequelize }) => {
20 | context.getQueryInterface
21 | //await context.getQueryInterface().removeX
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-pull-request.yml:
--------------------------------------------------------------------------------
1 | name: 'Deploy: Client: Firebase > Review App'
2 | on:
3 | pull_request:
4 | branches: ['master']
5 | paths:
6 | - 'workspaces/client/**'
7 |
8 | jobs:
9 | build_and_preview:
10 | if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - run: yarn workspace client build
15 | - uses: FirebaseExtended/action-hosting-deploy@v0
16 | with:
17 | repoToken: '${{ secrets.GITHUB_TOKEN }}'
18 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_DRAWSPACE_6C652 }}'
19 | projectId: drawspace-6c652
20 | entryPoint: 'workspaces/client'
21 |
--------------------------------------------------------------------------------
/workspaces/client/src/shared/selectFile.ts:
--------------------------------------------------------------------------------
1 | export function selectFile(contentType: string, multiple = false): Promise {
2 | return new Promise(resolve => {
3 | const input = document.createElement('input')
4 | input.type = 'file'
5 | input.multiple = multiple
6 | input.accept = contentType
7 | input.onchange = () => {
8 | const files = Array.from(input.files || [])
9 | resolve(files)
10 | }
11 | input.click()
12 | })
13 | }
14 |
15 | export async function selectJsonFile>(): Promise {
16 | const result = await selectFile('.json')
17 | if (!result[0]) {
18 | return null
19 | }
20 | const file = await result[0].text()
21 | const obj = JSON.parse(file)
22 | return obj as T
23 | }
24 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/app/App.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, waitFor } from '@testing-library/react'
2 | import App from './App'
3 | import { config } from '../../shared/config'
4 | import axios from 'axios'
5 | import MockAdapter from 'axios-mock-adapter'
6 |
7 | describe('App', () => {
8 | test('Root render', async () => {
9 | const axiosMock = new MockAdapter(axios)
10 | axiosMock.onGet('/config').reply(200, { ready: true })
11 | axiosMock.onGet('/gallery').reply(200, { mocked: true })
12 | const { findAllByRole } = render()
13 | await waitFor(() => findAllByRole('progressbar'), {
14 | timeout: 5000
15 | })
16 | const appLinks = screen.getAllByText(config.defaultTitle)
17 | expect(appLinks.length).toBeGreaterThan(0)
18 | }, 10000)
19 | })
20 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/slice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2 | import { PagedResult } from '../../../../lib/src/types'
3 |
4 | export interface AdminState {
5 | activeMenuItem?: string
6 | menuOpen?: boolean
7 | loading?: boolean
8 | loaded?: boolean
9 | data: {
10 | [key: string]: PagedResult
11 | }
12 | }
13 |
14 | const initialState: AdminState = {
15 | data: {},
16 | }
17 |
18 | const slice = createSlice({
19 | name: 'admin',
20 | initialState,
21 | reducers: {
22 | patch: (state, action: PayloadAction>) => {
23 | return { ...state, ...action.payload }
24 | },
25 | },
26 | })
27 |
28 | export const { patch } = slice.actions
29 |
30 | export const actions = slice.actions
31 |
32 | export default slice.reducer
33 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/shop/FakeCheckout.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 |
3 | import { PaymentIntentResult, PaymentIntent } from '@stripe/stripe-js'
4 | import { useAppDispatch } from '../../shared/store'
5 |
6 | import CircularProgress from '@mui/material/CircularProgress'
7 |
8 | import { checkoutAsync } from './thunks'
9 |
10 | export default function FakeCheckout({}: React.PropsWithChildren<{
11 | onApproval?: (result: PaymentIntentResult) => void
12 | onLoading?: () => void
13 | }>): JSX.Element {
14 | const dispatch = useAppDispatch()
15 |
16 | useEffect((): void => {
17 | dispatch(
18 | checkoutAsync({
19 | paymentIntent: {
20 | amount: 111
21 | } as PaymentIntent
22 | })
23 | )
24 | }, [dispatch])
25 |
26 | return
27 | }
28 |
--------------------------------------------------------------------------------
/workspaces/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": false,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "target": "ES2021",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "outDir": "build",
18 | "jsx": "react-jsx",
19 | "baseUrl": ".",
20 | "paths": {
21 | "@lib": ["../lib/src"]
22 | }
23 | },
24 | "include": ["src", ".eslintrc.*"],
25 | "exclude": ["node_modules", "build"],
26 | "references": [{ "path": "../lib" }]
27 | }
28 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Dashboard/images/earning.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/TabPanel.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@mui/material/Box'
2 |
3 | interface TabPanelProps extends React.HTMLAttributes {
4 | children?: React.ReactNode
5 | index?: number
6 | value?: number
7 | tabs?: string
8 | keepMounted?: boolean
9 | }
10 |
11 | export function TabPanel(props: TabPanelProps = { tabs: 'tabs' }) {
12 | const { children, value, index, tabs, keepMounted, ...other } = props
13 | const id = tabs?.toLowerCase().replace(/ /g, '-')
14 |
15 | return (
16 |
23 | {(value === index || keepMounted) && {children}}
24 |
25 | )
26 | }
27 |
28 | export default TabPanel
29 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/types/models/drawing.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes } from 'sequelize'
2 | import { Drawing } from '@lib'
3 | import { addModel } from '../../db/util'
4 |
5 | export const DrawingModel = addModel({
6 | name: 'drawing',
7 | attributes: {
8 | drawingId: {
9 | type: DataTypes.UUID,
10 | primaryKey: true,
11 | defaultValue: DataTypes.UUIDV4
12 | },
13 | userId: {
14 | type: DataTypes.UUID
15 | },
16 | name: {
17 | type: DataTypes.STRING
18 | },
19 | history: {
20 | type: DataTypes.JSONB
21 | },
22 | thumbnail: {
23 | type: DataTypes.TEXT
24 | },
25 | private: {
26 | type: DataTypes.BOOLEAN
27 | },
28 | sell: {
29 | type: DataTypes.BOOLEAN
30 | },
31 | price: {
32 | type: DataTypes.DECIMAL(10, 2)
33 | }
34 | }
35 | })
36 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/types/models/subscription.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes } from 'sequelize'
2 | import { addModel } from '../../db/util'
3 | import { Subscription } from '@lib'
4 |
5 | export const SubscriptionModel = addModel({
6 | name: 'subscription',
7 | attributes: {
8 | subscriptionId: {
9 | type: DataTypes.UUID,
10 | primaryKey: true,
11 | defaultValue: DataTypes.UUIDV4
12 | },
13 | userId: {
14 | type: DataTypes.UUID
15 | },
16 | orderId: {
17 | type: DataTypes.UUID
18 | },
19 | priceId: {
20 | type: DataTypes.STRING
21 | },
22 | status: {
23 | type: DataTypes.STRING
24 | },
25 | title: {
26 | type: DataTypes.STRING
27 | },
28 | canceledAt: {
29 | type: DataTypes.DATE
30 | },
31 | cancelationReason: {
32 | type: DataTypes.STRING
33 | }
34 | }
35 | })
36 |
--------------------------------------------------------------------------------
/workspaces/function-gkill/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gkill",
3 | "main": "src/index.js",
4 | "version": "0.0.1",
5 | "description": "Billing kill switch",
6 | "dependencies": {
7 | "@google-cloud/functions-framework": "^3.1.2",
8 | "@google-cloud/pubsub": "^3.2.1",
9 | "google-auth-library": "^8.8.0",
10 | "googleapis": "^118.0.0"
11 | },
12 | "devDependencies": {
13 | "@types/eslint": "^8",
14 | "@types/node": "^14.11.2",
15 | "@typescript-eslint/eslint-plugin": "^5.48.2",
16 | "eslint": "^8.42.0",
17 | "eslint-import-resolver-typescript": "^3.5.5",
18 | "eslint-plugin-import": "^2.27.5",
19 | "gts": "^3.1.1",
20 | "typescript": "^5.1.3"
21 | },
22 | "scripts": {
23 | "lint": "gts lint",
24 | "clean": "gts clean",
25 | "build": "yarn tsc --build",
26 | "gcp-build": ""
27 | },
28 | "engines": {
29 | "yarn": ">=3.30.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Dashboard/chart-data/bajaj-area-chart.ts:
--------------------------------------------------------------------------------
1 | // ===========================|| DASHBOARD - BAJAJ AREA CHART ||=========================== //
2 |
3 | const chartData = {
4 | type: 'area',
5 | height: 95,
6 | options: {
7 | chart: {
8 | id: 'support-chart',
9 | sparkline: {
10 | enabled: true,
11 | },
12 | },
13 | dataLabels: {
14 | enabled: false,
15 | },
16 | stroke: {
17 | curve: 'smooth',
18 | width: 1,
19 | },
20 | tooltip: {
21 | fixed: {
22 | enabled: false,
23 | },
24 | x: {
25 | show: false,
26 | },
27 | y: {
28 | title: 'Ticket ',
29 | },
30 | marker: {
31 | show: false,
32 | },
33 | },
34 | },
35 | series: [
36 | {
37 | data: [0, 15, 10, 50, 30, 40, 25],
38 | },
39 | ],
40 | }
41 |
42 | export default chartData
43 |
--------------------------------------------------------------------------------
/bin/compile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # bin/compile
3 |
4 | # used for inline heroku buildpack
5 |
6 | BUILD_DIR=${1:-}
7 | CACHE_DIR=${2:-}
8 | ENV_DIR=${3:-}
9 |
10 | PROJECT_PATH=src
11 |
12 | echo "-----> monorepo post cleanup buildpack: path is $PROJECT_PATH"
13 | # npm list
14 | # echo " creating cache: $CACHE_DIR"
15 | # mkdir -p $CACHE_DIR
16 | # TMP_DIR=`mktemp -d $CACHE_DIR/subdirXXXXX`
17 | # echo " created tmp dir: $TMP_DIR"
18 |
19 | # echo " moving working dir: $PROJECT_PATH to $TMP_DIR"
20 | # cp -R $BUILD_DIR/$PROJECT_PATH/. $TMP_DIR/
21 |
22 | # echo " cleaning build dir $BUILD_DIR"
23 | # rm -rf $BUILD_DIR
24 | # echo " recreating $BUILD_DIR"
25 | # mkdir -p $BUILD_DIR
26 | # echo " copying work dir from cache $TMP_DIR to build dir $BUILD_DIR"
27 | # cp -R $TMP_DIR/. $BUILD_DIR/
28 | # echo " cleaning tmp dir $TMP_DIR"
29 | # rm -rf $TMP_DIR
30 | exit 0
31 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/canvas/Player.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { DrawAction } from '@lib'
3 | // import { useAppSelector } from '../../shared/store'
4 | import StyledSlider from '../ui/StyledSlider'
5 | import Box from '@mui/material/Box'
6 |
7 | export default function Player({ buffer }: { buffer: React.RefObject }) {
8 | // const active = useAppSelector((state) => state.canvas?.active)
9 | const max = buffer?.current?.length || 1
10 | const [marks, setMarks] = React.useState<{ value: number; label: string }[]>([])
11 |
12 | useEffect(() => {
13 | const m = [{ value: 0, label: '0' }]
14 | if (max > 0) {
15 | m.push({ value: max, label: `${max}` })
16 | }
17 | setMarks(m)
18 | }, [max])
19 |
20 | return (
21 |
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/types/models/product.ts:
--------------------------------------------------------------------------------
1 | import { Product } from '@lib'
2 | import { DataTypes } from 'sequelize'
3 | import { addModel } from '../../db/util'
4 | // import { CategoryModel } from './category'
5 |
6 | export const ProductDefinition = {
7 | productId: {
8 | type: DataTypes.STRING,
9 | primaryKey: true
10 | },
11 | title: {
12 | type: DataTypes.STRING,
13 | required: true
14 | },
15 | description: {
16 | type: DataTypes.STRING
17 | },
18 | imageUrl: {
19 | type: DataTypes.STRING
20 | },
21 | images: {
22 | type: DataTypes.ARRAY(DataTypes.STRING)
23 | },
24 | keywords: {
25 | type: DataTypes.STRING
26 | },
27 | prices: {
28 | type: DataTypes.JSONB
29 | },
30 | shippable: {
31 | type: DataTypes.BOOLEAN,
32 | defaultValue: false
33 | }
34 | }
35 |
36 | export const ProductModel = addModel({ name: 'product', attributes: ProductDefinition })
37 |
--------------------------------------------------------------------------------
/docs/dev.md:
--------------------------------------------------------------------------------
1 | ## debug
2 |
3 |
4 | ## Commit Flow
5 | `epic | feature -> develop -> master`
6 | - Feature branches are created from, merged to, develop
7 | - Pre-commit compile check
8 |
9 | ## Push Flow
10 | - Pre-push tests against docker environment, push if okay
11 | - Github Action Tests are run against testing environment
12 | - If tests pass, build container with branch tag
13 | - Push built container image to registry
14 | - Trigger environment redeployment
15 |
16 | | Branch | Environment |
17 | | --- | --- |
18 | | master | Production |
19 | | develop | Testing |
20 | | feature | Review App |
21 | | epic | Review App |
22 | | epic-feature | none |
23 |
24 |
25 | ## Container Flow
26 |
27 | Image Tags are based on the branch name and not hash, with latest being environment source
28 |
29 | `Branch -> Tests -> Container -> Environment`
30 |
31 | Github Actions can use the branch name hints like `review` for review app and web hook creation
32 |
33 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Notes
2 |
3 | ## State Management
4 |
5 | - Redux for app state machine
6 | - React Query for data retrieval and caching at component level
7 | - useState as normal
8 |
9 | ## Space
10 |
11 | - Not storing canvas image data directly, rather an array[] buffer of CanvasAction(s)
12 | - A Lean object made to be stored in database as well as consumed by worker to redraw image in the background
13 | - Thumbnails stored as data-image/png
14 |
15 | ## Data Flow
16 |
17 | - API requests centralized via request() function with typings, side effects and errors
18 | - Components use custom useGet hook for data retrievals
19 | - Patching slice state done via generic actions.patch({ prop: value })
20 |
21 | ## Layout
22 |
23 | - Flexbox strategy for responsiveness via css
24 | - Flexed routed section for MaterialUI content without margins
25 |
26 | ## Styles
27 |
28 | - Global: index.css
29 | - Small: sx
30 | - Medium: const
31 | - Big: Component.styles.ts
32 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/home/images/github.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/workspaces/server/src/routes/swagger.yaml:
--------------------------------------------------------------------------------
1 | components:
2 | securitySchemes:
3 | BearerAuth:
4 | type: http
5 | scheme: bearer
6 | in: header
7 | responses:
8 | 403Unauthorized:
9 | description: You do not have permission to access this
10 | content:
11 | application/json:
12 | schema:
13 | type: object
14 | properties:
15 | statusCode:
16 | type: integer
17 | example: 403
18 | message:
19 | type: string
20 | example: Unauthorized
21 | 500Error:
22 | description: Unable to process the request
23 | content:
24 | application/json:
25 | schema:
26 | type: object
27 | properties:
28 | statusCode:
29 | type: integer
30 | example: 500
31 | message:
32 | type: string
33 | example: Internal Server Error
34 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-live.yml:
--------------------------------------------------------------------------------
1 | name: 'Deploy: Client: Firebase > Live'
2 | on:
3 | push:
4 | branches:
5 | - master
6 | paths:
7 | - 'workspaces/client/**'
8 | - '.github/workflows/firebase-hosting-live.yml'
9 |
10 | jobs:
11 | build_and_deploy:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - uses: actions/setup-node@v3
17 | with:
18 | node-version: 18
19 | cache: 'yarn'
20 |
21 | - name: 'install'
22 | run: yarn install --immutable
23 |
24 | - name: 'build'
25 | run: |
26 | yarn workspace client build
27 |
28 | - uses: FirebaseExtended/action-hosting-deploy@v0
29 | with:
30 | repoToken: '${{ secrets.GITHUB_TOKEN }}'
31 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_DRAWSPACE_6C652 }}'
32 | channelId: live
33 | projectId: drawspace-6c652
34 | entryPoint: 'workspaces/client'
35 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/CircularProgressWithLabel.tsx:
--------------------------------------------------------------------------------
1 | import CircularProgress, { CircularProgressProps } from '@mui/material/CircularProgress'
2 | import Typography from '@mui/material/Typography'
3 | import Box from '@mui/material/Box'
4 |
5 | export default function CircularProgressWithLabel(
6 | props: CircularProgressProps & { value: number },
7 | ) {
8 | return (
9 |
10 |
11 |
23 | {`${Math.round(
24 | props.value,
25 | )}%`}
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/workspaces/server/src/config/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "development": {
3 | "db": {
4 | "url": null,
5 | "username": "postgres",
6 | "password": "postgrespass",
7 | "database": "drawspace",
8 | "schema": "public",
9 | "host": "127.0.0.1",
10 | "ssl": false,
11 | "dialect": "postgres"
12 | },
13 | "service": {
14 | "port": 3001,
15 | "protocol": "http",
16 | "host": "localhost",
17 | "sslKey": null,
18 | "sslCert": null
19 | }
20 | },
21 | "production": {
22 | "db": {
23 | "url": "$DB_URL",
24 | "schema": "public",
25 | "ssl": false
26 | },
27 | "service": {
28 | "port": 8080,
29 | "protocol": "http",
30 | "host": "localhost",
31 | "sslKey": "$SSL_KEY",
32 | "sslCert": "$SSL_CERT"
33 | }
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/types/models/wallet.ts:
--------------------------------------------------------------------------------
1 | import { Wallet, WalletTransaction } from '@lib'
2 | import { DataTypes } from 'sequelize'
3 | import { addModel } from '../../db/util'
4 |
5 | export const WalletModel = addModel({
6 | name: 'wallet',
7 | attributes: {
8 | walletId: {
9 | type: DataTypes.UUID,
10 | primaryKey: true
11 | },
12 | balance: {
13 | type: DataTypes.DECIMAL(10, 2)
14 | },
15 | currency: {
16 | type: DataTypes.STRING
17 | },
18 | status: {
19 | type: DataTypes.STRING
20 | }
21 | }
22 | })
23 |
24 | export const WalletTransactionModel = addModel({
25 | name: 'walletTransaction',
26 | attributes: {
27 | transactionId: {
28 | type: DataTypes.UUID,
29 | primaryKey: true
30 | },
31 | userId: {
32 | type: DataTypes.UUID
33 | },
34 | orderId: {
35 | type: DataTypes.UUID
36 | },
37 | amount: {
38 | type: DataTypes.STRING
39 | }
40 | }
41 | })
42 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/secrets.ts:
--------------------------------------------------------------------------------
1 | import { v1 } from '@google-cloud/secret-manager'
2 | import logger from './logger'
3 |
4 | export async function getSecrets() {
5 | logger.info(`Getting secrets from Secret Manager...`)
6 | const result = {} as { [key: string]: string }
7 | const client = new v1.SecretManagerServiceClient({
8 | projectId: 'mstream-368503',
9 | })
10 | const asyncList = client.listSecretsAsync()
11 | try {
12 | for await (const secret of asyncList) {
13 | const name = secret.name as string
14 | if (!name) {
15 | logger.error(`Secret name is missing`)
16 | continue
17 | }
18 | const [details] = await client.accessSecretVersion({ name })
19 | logger.info(`Secret: ${name} = ${JSON.stringify(details)}`)
20 | if (!details.payload?.data) {
21 | result[name] = details.payload?.data as string
22 | }
23 | }
24 | } catch (error) {
25 | logger.error(`Error getting secrets: ${error}`)
26 | }
27 | return result
28 | }
29 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/home/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import Container from '@mui/material/Container'
3 | import Gallery from '../ui/Gallery'
4 | import HeroSection from './HeroSection'
5 | import Logos from './Logos'
6 | import Box from '@mui/material/Box'
7 | import Typography from '@mui/material/Typography'
8 | import { Divider, Paper } from '@mui/material'
9 | import Footer from './Footer'
10 | import Subscribe from './Subscribe'
11 | export default function HomePage() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Drawings Marketplace
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/workspaces/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lib",
3 | "version": "1.0.5",
4 | "private": true,
5 | "exports": {
6 | ".": "./dist/src/index.js",
7 | "./package.json": "./package.json"
8 | },
9 | "scripts": {
10 | "dev": "yarn tsc --watch --verbose",
11 | "build": "yarn tsc --build",
12 | "lint": "eslint .",
13 | "test": "yarn jest",
14 | "clean": "rm -rf node_modules dist"
15 | },
16 | "dependencies": {
17 | "uuid": "^9.0.0"
18 | },
19 | "devDependencies": {
20 | "@types/uuid": "^8.3.4",
21 | "@typescript-eslint/eslint-plugin": "^5.57.1",
22 | "@typescript-eslint/parser": "^5.30.6",
23 | "eslint": "^8.42.0",
24 | "eslint-config-standard": "^17.1.0",
25 | "eslint-import-resolver-typescript": "^3.5.5",
26 | "eslint-plugin-import": "^2.27.5",
27 | "eslint-plugin-n": "^16.0.0",
28 | "eslint-plugin-promise": "^6.1.1",
29 | "eslint-plugin-react": "^7.32.2",
30 | "jest": "^29.5.0",
31 | "ts-jest": "^29.1.0",
32 | "typescript": "^5.1.3"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/canvas/LineSize.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@mui/material/styles'
2 | import { config } from '../../shared/config'
3 | import { useAppDispatch, useAppSelector } from '../../shared/store'
4 | import { actions } from './slice'
5 | import Container from '@mui/material/Container'
6 | import Slider from '@mui/material/Slider'
7 |
8 | const ContainerStyled = styled(Container)({
9 | position: 'absolute',
10 | bottom: '3%',
11 | left: '20%',
12 | width: '70%'
13 | })
14 |
15 | export default function LineSize() {
16 | const activeSize = useAppSelector(state => state.canvas.size)
17 | const dispatch = useAppDispatch()
18 | const onSize = (e: Event, v: number | number[]) => dispatch(actions.patch({ size: v as number }))
19 | return (
20 |
21 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Card/Skeleton/EarningCard.tsx:
--------------------------------------------------------------------------------
1 | import Card from '@mui/material/Card'
2 | import CardContent from '@mui/material/CardContent'
3 | import Grid from '@mui/material/Grid'
4 | import Skeleton from '@mui/material/Skeleton'
5 |
6 | const EarningCard = () => (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | )
30 |
31 | export default EarningCard
32 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/PasswordField.tsx:
--------------------------------------------------------------------------------
1 | import Visibility from '@mui/icons-material/Visibility'
2 | import VisibilityOff from '@mui/icons-material/VisibilityOff'
3 | import InputAdornment from '@mui/material/InputAdornment'
4 | import TextField, { TextFieldProps } from '@mui/material/TextField'
5 |
6 | import React from 'react'
7 |
8 | export default function PasswordField(props: TextFieldProps) {
9 | const [visible, setVisible] = React.useState(false)
10 | return (
11 | {
22 | setVisible(!visible)
23 | }}
24 | >
25 | {visible ? : }
26 |
27 | )
28 | }}
29 | {...props}
30 | />
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Footer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Box from '@mui/material/Box'
3 | import BottomNavigation from '@mui/material/BottomNavigation'
4 | import BottomNavigationAction from '@mui/material/BottomNavigationAction'
5 | import RestoreIcon from '@mui/icons-material/Restore'
6 | import FavoriteIcon from '@mui/icons-material/Favorite'
7 | import LocationOnIcon from '@mui/icons-material/LocationOn'
8 |
9 | export default function SimpleBottomNavigation() {
10 | const [value, setValue] = React.useState(0)
11 |
12 | return (
13 |
14 | {
18 | setValue(newValue)
19 | }}
20 | >
21 | } />
22 | } />
23 | } />
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/workspaces/server/src/index.ts:
--------------------------------------------------------------------------------
1 | import config, { canStart } from './shared/config'
2 | import logger from './shared/logger'
3 | import createBackendApp from './app'
4 | import { registerSocket } from './shared/socket'
5 | import { createServerService } from './shared/server'
6 | ;(() => {
7 | if (!canStart()) {
8 | const m = 'No PORT specified: Shutting down - Environment variables undefined'
9 | logger.error(m)
10 | throw new Error(m)
11 | }
12 |
13 | const app = createBackendApp()
14 | const url = config.backendBaseUrl + config.swaggerSetup.basePath
15 | const title = config.swaggerSetup.info?.title
16 |
17 | // Start server
18 | const server = createServerService(app)
19 | registerSocket(server)
20 |
21 | server.listen(config.port, () =>
22 | logger.info(
23 | `**************************************************************************\n\
24 | ⚡️[${title}]: Server is running with SwaggerUI Admin at ${url}\n\
25 | **************************************************************************`,
26 | ),
27 | )
28 | return server
29 | })()
30 |
--------------------------------------------------------------------------------
/workspaces/client/src/shared/config.ts:
--------------------------------------------------------------------------------
1 | import { ClientSettings } from '@lib'
2 | import packageJson from '../../package.json'
3 |
4 | /**
5 | * move most to redux and settings
6 | */
7 | export interface Config {
8 | baseName: string
9 | backendUrl: string
10 | defaultTitle: string
11 | defaultLineSize: number
12 | defaultColor: string
13 | thumbnails: {
14 | width: number
15 | height: number
16 | }
17 | settings: ClientSettings
18 | admin: {
19 | path: string
20 | models?: string[]
21 | }
22 | }
23 | const env = process['env']
24 | const defaultBaseName = process.env.NODE_ENV === 'test' ? '/' : packageJson.homepage || '/'
25 | export const config: Config = {
26 | baseName: env.BASE_NAME || defaultBaseName,
27 | backendUrl: env.BACKEND || 'https://api.drawspace.app',
28 | defaultTitle: 'Drawspace',
29 | defaultColor: 'green',
30 | defaultLineSize: 5,
31 | thumbnails: {
32 | width: 250,
33 | height: 250,
34 | },
35 | settings: {},
36 | admin: {
37 | path: '/admin',
38 | models: [],
39 | },
40 | }
41 |
42 | export default config
43 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/pages/Maintenance.tsx:
--------------------------------------------------------------------------------
1 | import Paper from '@mui/material/Paper'
2 | import image from './images/maintenance.svg'
3 | import Grid from '@mui/material/Grid'
4 | import Typography from '@mui/material/Typography'
5 | export default function Maintenance() {
6 | return (
7 |
8 |
20 |
21 |
22 | Offline for Maintenance
23 |
24 |
25 | Please come back later
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/workspaces/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "commonjs": true,
6 | "node": true,
7 | "es6": true,
8 | "jest": true
9 | },
10 | "extends": ["plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:react/jsx-runtime"],
11 | "parserOptions": {
12 | "ecmaVersion": "latest",
13 | "sourceType": "module",
14 | "project": "workspaces/*/tsconfig.json"
15 | },
16 | "parser": "@typescript-eslint/parser",
17 | "plugins": ["react", "react-hooks", "@typescript-eslint"],
18 | "rules": {
19 | "@typescript-eslint/no-unused-vars": "error",
20 | "@typescript-eslint/explicit-function-return-type": "off",
21 | "@typescript-eslint/restrict-template-expressions": "off",
22 | "@typescript-eslint/strict-boolean-expressions": "off",
23 | "no-console": "off",
24 | "strict": ["error", "global"],
25 | "curly": "warn"
26 | },
27 | "overrides": [
28 | {
29 | "files": ["*.js"],
30 | "rules": {
31 | "@typescript-eslint/no-var-requires": "off"
32 | }
33 | }
34 | ]
35 | }
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Card/Skeleton/TotalIncomeCard.tsx:
--------------------------------------------------------------------------------
1 | // material-ui
2 |
3 | import Card from '@mui/material/Card'
4 | import List from '@mui/material/List'
5 | import ListItem from '@mui/material/ListItem'
6 | import ListItemAvatar from '@mui/material/ListItemAvatar'
7 | import ListItemText from '@mui/material/ListItemText'
8 | import Skeleton from '@mui/material/Skeleton'
9 |
10 | // ==============================|| SKELETON - TOTAL INCOME DARK/LIGHT CARD ||============================== //
11 |
12 | const TotalIncomeCard = () => (
13 |
14 |
15 |
16 |
17 |
18 |
19 | }
22 | secondary={}
23 | />
24 |
25 |
26 |
27 | )
28 |
29 | export default TotalIncomeCard
30 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/firebase.ts:
--------------------------------------------------------------------------------
1 | import { initializeApp, cert, App } from 'firebase-admin/app'
2 | import { getSettingsAsync } from './settings'
3 |
4 | let firebaseApp: App | null = null
5 | export const getFirebaseApp = async (): Promise => {
6 | if (firebaseApp) {
7 | return firebaseApp
8 | }
9 |
10 | const settings = await getSettingsAsync(true)
11 | const serviceAccountKeyJson = settings.internal?.secrets?.google.serviceAccountJson
12 |
13 | if (!serviceAccountKeyJson) {
14 | throw new Error('To use firebase authentication you need to input your serviceAccountKey.json')
15 | }
16 |
17 | const serviceAccountObject = JSON.parse(serviceAccountKeyJson || '{}')
18 | const projectId = serviceAccountObject?.project_id
19 | const storageBucket = `${projectId}.appspot.com`
20 | const databaseURL = `https://${projectId}.firebaseio.com`
21 | const credential = serviceAccountObject ? cert(serviceAccountObject) : undefined
22 | firebaseApp = initializeApp({
23 | credential,
24 | databaseURL,
25 | storageBucket
26 | })
27 | return firebaseApp as App
28 | }
29 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/app/LanguageProvider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { IntlProvider } from 'react-intl'
3 | import { useAppSelector } from '../../shared/store'
4 |
5 | export type IntlProviderProps = React.ComponentProps
6 |
7 | const cache: Record> = {}
8 |
9 | async function loadLocale(locale: string): Promise> {
10 | if (cache[locale]) {
11 | return cache[locale]
12 | }
13 | cache[locale] = await import(`../languages/${locale}.json`)
14 | return cache[locale]
15 | }
16 |
17 | export default function LanguageProvider(props: { children: React.ReactElement }) {
18 | const { children } = props
19 | const locale = useAppSelector(state => state.app.locale)
20 | const [messages, setMessages] = React.useState>({})
21 | React.useEffect(() => {
22 | loadLocale(locale).then(res => setMessages(res))
23 | }, [locale])
24 |
25 | return (
26 |
27 | {children}
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/shop/Receipt.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@mui/material/Box'
2 | import Stack from '@mui/material/Stack'
3 | import Typography from '@mui/material/Typography'
4 | import { useAppSelector } from 'src/shared/store'
5 |
6 | export default function Receipt() {
7 | const order = useAppSelector(state => state.shop.receipt)
8 | return (
9 |
10 |
11 | Thank you
12 |
13 |
14 |
22 | Your order was placed succesfully!
23 | {order?.orderId}
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/home/images/redux.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/MainLayout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Header from './Header'
3 | import DrawerRight from './Drawer'
4 | import Routing, { RouteElement } from './Routing'
5 | import Notifications from './Notifications'
6 | import Footer from './Footer'
7 | import LoadingLine from './LoadingLine'
8 | import AuthProviderInjections from '../profile/AuthProviders'
9 | import { currentRoute } from '../../shared/routes'
10 | import Dialogs from './Dialogs'
11 | import CssBaseline from '@mui/material/CssBaseline'
12 | import SocketListener from '../app/SocketListener'
13 |
14 | export function MainLayout() {
15 | const route = currentRoute()
16 | if (route?.cleanLayout) {
17 | return
18 | }
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "commonjs": true,
6 | "node": true,
7 | "es6": true,
8 | "jest": true
9 | },
10 | "extends": ["plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:react/jsx-runtime"],
11 | "ignorePatterns": ["**/dist", "**/node_modules", "**/build"],
12 | "parserOptions": {
13 | "ecmaVersion": "latest",
14 | "sourceType": "module",
15 | "project": "*/tsconfig.json"
16 | },
17 | "parser": "@typescript-eslint/parser",
18 | "plugins": ["react", "react-hooks", "@typescript-eslint"],
19 | "rules": {
20 | "@typescript-eslint/no-unused-vars": "error",
21 | "@typescript-eslint/explicit-function-return-type": "off",
22 | "@typescript-eslint/restrict-template-expressions": "off",
23 | "@typescript-eslint/strict-boolean-expressions": "off",
24 | "no-console": "off",
25 | "strict": ["error", "global"],
26 | "curly": "warn"
27 | },
28 | "overrides": [
29 | {
30 | "files": ["*.js"],
31 | "rules": {
32 | "@typescript-eslint/no-var-requires": "off"
33 | }
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Drawer.tsx:
--------------------------------------------------------------------------------
1 | import SwipeableDrawer from '@mui/material/SwipeableDrawer'
2 | import { useAppSelector, useAppDispatch } from '../../shared/store'
3 | import { patch } from '../app/slice'
4 | import Cart from '../shop/ShopCart'
5 | import Box from '@mui/material/Box'
6 | import Card from '@mui/material/Card'
7 |
8 | // Clipped items listing
9 | export default function DrawerRight() {
10 | const dispatch = useAppDispatch()
11 | const open = useAppSelector(store => store.app.drawerRightOpen)
12 | const toggleOpen = () => {
13 | dispatch(patch({ drawerRightOpen: !open }))
14 | }
15 | return (
16 | ''}
21 | ModalProps={{
22 | keepMounted: true
23 | }}
24 | >
25 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/subscription.ts:
--------------------------------------------------------------------------------
1 | import { Entity, Order, PaymentSource } from '.'
2 |
3 | export const PlanIntervals = {
4 | Day: 'day',
5 | Week: 'week',
6 | Month: 'month',
7 | Year: 'year'
8 | } as const
9 |
10 | export type PlanInterval = typeof PlanIntervals[keyof typeof PlanIntervals]
11 |
12 | export const PlanStatus = {
13 | Active: 'active',
14 | Expired: 'expired',
15 | Canceled: 'canceled'
16 | } as const
17 |
18 | export type PlanStatus = typeof PlanStatus[keyof typeof PlanStatus]
19 |
20 | export interface SubscriptionPlan extends Entity {
21 | subscriptionPlanId: string
22 | name: string
23 | description?: string
24 | amount?: number
25 | interval?: PlanInterval
26 | intervalCount?: number
27 | trialPeriodDays?: number
28 | mappings: { [key in PaymentSource]?: { productId: string; enabled: boolean } }
29 | enabled?: boolean
30 | }
31 |
32 | export interface Subscription extends Entity {
33 | subscriptionId?: string
34 | userId?: string
35 | orderId?: string
36 | priceId?: string
37 | title?: string
38 | status?: PlanStatus
39 | canceledAt?: Date
40 | cancelationReason?: string
41 | order?: Order
42 | }
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /**/node_modules
5 | /**/.pnp
6 | /**/.pnp.js
7 |
8 | # testing
9 | /**/coverage
10 |
11 | # production
12 | /**/dist
13 | /**/build
14 |
15 | certs
16 | workspaces/server/src/certs
17 |
18 | # misc
19 | .DS_Store
20 | .env
21 | .env.local
22 | .env.development.local
23 | .env.test.local
24 | .env.production.local
25 |
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 | *.tsbuildinfo
30 | *.log
31 | *.cache
32 | .rem
33 |
34 | #amplify-do-not-edit-begin
35 | amplify/\#current-cloud-backend
36 | amplify/.config/local-*
37 | amplify/logs
38 | amplify/mock-data
39 | amplify/backend/amplify-meta.json
40 | amplify/backend/.temp
41 | build/
42 | dist/
43 | node_modules/
44 | aws-exports.js
45 | awsconfiguration.json
46 | amplifyconfiguration.json
47 | amplifyconfiguration.dart
48 | amplify-build-config.json
49 | amplify-gradle-config.json
50 | amplifytools.xcconfig
51 | .secret-*
52 | **.sample
53 | #amplify-do-not-edit-end
54 |
55 | # yarn2 - NOT using Zero-Installs
56 | .pnp.*
57 | .yarn/*
58 | !.yarn/patches
59 | !.yarn/plugins
60 | !.yarn/releases
61 | !.yarn/sdks
62 | !.yarn/versions
63 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/types/models/cart.ts:
--------------------------------------------------------------------------------
1 | import { Cart } from '@lib'
2 | import { DataTypes } from 'sequelize'
3 | import { addModel } from '../../db/util'
4 | import { DrawingModel } from './drawing'
5 | import { ProductModel } from './product'
6 |
7 | export const CartAttributes = {
8 | cartId: {
9 | primaryKey: true,
10 | type: DataTypes.UUID,
11 | defaultValue: DataTypes.UUIDV4
12 | },
13 | userId: {
14 | type: DataTypes.UUID
15 | },
16 | cartType: {
17 | type: DataTypes.STRING
18 | },
19 | productId: {
20 | type: DataTypes.STRING
21 | },
22 | priceId: {
23 | type: DataTypes.STRING
24 | },
25 | drawingId: {
26 | type: DataTypes.UUID
27 | },
28 | quantity: {
29 | type: DataTypes.INTEGER
30 | }
31 | }
32 |
33 | export const CartModel = addModel({
34 | name: 'cart',
35 | attributes: CartAttributes,
36 | joins: [
37 | {
38 | type: 'belongsTo',
39 | target: DrawingModel,
40 | foreignKey: 'drawingId',
41 | as: 'drawing'
42 | },
43 | {
44 | type: 'belongsTo',
45 | target: ProductModel,
46 | foreignKey: 'productId',
47 | as: 'product'
48 | }
49 | ]
50 | })
51 |
52 | export default CartModel
53 |
--------------------------------------------------------------------------------
/workspaces/lib/src/util.ts:
--------------------------------------------------------------------------------
1 | import { validate, v4 } from 'uuid'
2 |
3 | export function dashifyUUID(i: string): string {
4 | return (
5 | i.substring(0, 8) +
6 | '-' +
7 | i.substring(8, 4) +
8 | '-' +
9 | i.substring(12, 4) +
10 | '-' +
11 | i.substring(16, 4) +
12 | '-' +
13 | i.substring(20)
14 | )
15 | }
16 |
17 | export function tryDashesOrNewUUID(undashed?: string): string {
18 | if (undashed) {
19 | const candidate = dashifyUUID(undashed)
20 | if (validate(candidate)) {
21 | return candidate
22 | }
23 | }
24 | return v4()
25 | }
26 |
27 | export function getPictureMock(payload: Record): string {
28 | let f = '?'
29 | let l = ''
30 | if (!payload?.picture && payload.firstName && payload.lastName) {
31 | f = payload.firstName.charAt(0).toLowerCase()
32 | l = payload.lastName.charAt(0).toLowerCase()
33 | }
34 | return `https://i2.wp.com/cdn.auth0.com/avatars/${f}${l}.png?ssl=1`
35 | }
36 |
37 | export type $ValuesOf = T[keyof T]
38 |
39 | export const MoneyFormat = Intl.NumberFormat('en-US', {
40 | style: 'currency',
41 | currency: 'USD'
42 | }).format
43 | export const toMoney = (value?: number) => (value ? MoneyFormat(value) : '')
44 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "configurations": [
3 | {
4 | "command": "yarn workspace server dev",
5 | "name": "dev",
6 | "request": "launch",
7 | "type": "node-terminal",
8 | "preLaunchTask": "client.watch"
9 | },
10 | {
11 | "command": "yarn workspace server prod",
12 | "name": "prod",
13 | "request": "launch",
14 | "type": "node-terminal"
15 | },
16 | {
17 | "command": "yarn workspace client dev",
18 | "name": "client",
19 | "request": "launch",
20 | "type": "node-terminal",
21 | "cwd": "${workspaceRoot}/workspaces/client"
22 | },
23 | {
24 | "command": "yarn workspace server dev",
25 | "name": "server",
26 | "request": "launch",
27 | "type": "node-terminal"
28 | },
29 | {
30 | "command": "yarn jest ${fileBasename} --watchAll=false --watch --runInBand",
31 | "name": "jest: $file",
32 | "request": "launch",
33 | "type": "node-terminal",
34 | "preLaunchTask": "docker.start"
35 | },
36 | {
37 | "command": "node ${fileDirname}/${fileBasename}",
38 | "name": "node: $file",
39 | "request": "launch",
40 | "type": "node-terminal"
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/types/models/item.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes } from 'sequelize'
2 | import { EntityDefinition, Join } from '../../db'
3 | import { Item } from '@lib'
4 | import { CategoryModel } from './category'
5 | import { addModel } from '../../db/util'
6 |
7 | export const ItemDefinition: EntityDefinition- = {
8 | itemId: {
9 | type: DataTypes.UUID,
10 | primaryKey: true
11 | },
12 | title: {
13 | type: DataTypes.STRING
14 | },
15 | urlName: {
16 | type: DataTypes.STRING
17 | },
18 | subscriptions: {
19 | type: DataTypes.ARRAY(DataTypes.STRING)
20 | },
21 | tokens: {
22 | type: DataTypes.INTEGER
23 | },
24 | price: {
25 | type: DataTypes.DECIMAL(10, 2)
26 | },
27 | currency: {
28 | type: DataTypes.STRING
29 | },
30 | inventory: {
31 | type: DataTypes.INTEGER
32 | },
33 | paywall: {
34 | type: DataTypes.BOOLEAN
35 | }
36 | }
37 |
38 | const joins: Join[] = [
39 | {
40 | target: CategoryModel,
41 | through: 'itemCategory',
42 | type: 'belongsToMany',
43 | foreignKey: 'itemId',
44 | otherKey: 'categoryId'
45 | }
46 | ]
47 |
48 | export const ItemModel = addModel
- ({
49 | name: 'item',
50 | attributes: ItemDefinition,
51 | joins
52 | })
53 |
--------------------------------------------------------------------------------
/workspaces/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import { Order } from './order'
2 | import { Wallet } from './wallet'
3 |
4 | export * from './cart'
5 | export * from './drawing'
6 | export * from './order'
7 | export * from './setting'
8 | export * from './user'
9 | export * from './subscription'
10 | export * from './product'
11 | export * from './wallet'
12 | export * from './auth'
13 | export * from './item'
14 | export * from './category'
15 |
16 | /**
17 | * Common Model Options
18 | * ie: Timestamps
19 | */
20 | export interface Entity {
21 | createdAt?: Date
22 | updatedAt?: Date
23 | deletedAt?: Date
24 | }
25 |
26 | export interface PagedResult {
27 | items?: T[]
28 | offset: number
29 | limit: number
30 | hasMore: boolean
31 | total: number
32 | }
33 |
34 | export interface GridPatchProps {
35 | id: string | number
36 | field: string
37 | value: unknown
38 | }
39 |
40 | export interface CheckoutRequest {
41 | ids: { cartId?: string; productId?: string; drawingId?: string }[]
42 | intent?: { amount: number; currency: string }
43 | confirmation?: string
44 | shippingAddressId?: string
45 | paymentSource?: string
46 | }
47 |
48 | export interface CheckoutResponse {
49 | order: Order
50 | error: string
51 | wallet?: Wallet
52 | }
53 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Dashboard/images/social-google.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/workspaces/client/src/shared/loadConfig.ts:
--------------------------------------------------------------------------------
1 | import { ClientConfig } from '@lib'
2 | import axios from 'axios'
3 | import { patch } from '../features/app'
4 | import config from './config'
5 | import store from './store'
6 |
7 | export default async function loadConfig() {
8 | // Remotish config
9 | const serverConfig = (await axios.get('/config'))?.data
10 | if (!serverConfig) {
11 | return
12 | }
13 | setConfig(serverConfig)
14 | return serverConfig
15 | }
16 |
17 | export function setConfig(payload: ClientConfig) {
18 | if (!payload || Object.keys(payload).length === 0) {
19 | return
20 | }
21 |
22 | const fromServer = payload as unknown as { [key: string]: unknown }
23 | // update config - TODO, reduce or remove config
24 | const indexed = config as unknown as { [key: string]: unknown }
25 | Object.keys(fromServer).forEach((key: string) => {
26 | if (typeof fromServer[key] === 'object') {
27 | indexed[key] = {
28 | ...(indexed[key] as { [key: string]: unknown }),
29 | ...(fromServer[key] as object)
30 | }
31 | } else {
32 | indexed[key] = fromServer[key]
33 | }
34 | })
35 | // update state
36 | store.dispatch(
37 | patch({
38 | ...fromServer,
39 | loaded: true
40 | })
41 | )
42 | return fromServer
43 | }
44 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/user.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from '.'
2 |
3 | export const UserRoles = {
4 | ADMIN: 'admin',
5 | MANAGER: 'manager'
6 | } as const
7 |
8 | export type UserRoleType = typeof UserRoles[keyof typeof UserRoles]
9 |
10 | export interface UserPreferences {
11 | [key: string]: unknown
12 | }
13 |
14 | export interface User extends Entity {
15 | userId: string
16 | email: string
17 | firstName?: string
18 | lastName?: string
19 | picture?: string
20 | banned?: boolean
21 | status?: number
22 | preferences?: UserPreferences
23 | loginCount?: number
24 | lastLogin?: Date
25 | roles?: string[]
26 | }
27 |
28 | export interface Address extends Entity {
29 | addressId: string
30 | userId: string
31 | name: string
32 | address1: string
33 | address2?: string
34 | city: string
35 | state: string
36 | zip: string
37 | country: string
38 | phone: string
39 | favorite?: boolean
40 | }
41 |
42 | export interface PaymentMethod extends Entity {
43 | paymentMethodId: string
44 | userId: string
45 | name: string
46 | type: string
47 | last4: string
48 | expMonth: number
49 | expYear: number
50 | favorite?: boolean
51 | }
52 |
53 | export interface UserActive {
54 | socketId: string
55 | userId: string
56 | ip?: string
57 | userAgent?: string
58 | }
59 |
--------------------------------------------------------------------------------
/workspaces/client/jest.config.js:
--------------------------------------------------------------------------------
1 | const { pathsToModuleNameMapper } = require('ts-jest')
2 | const { compilerOptions } = require('./tsconfig.json')
3 |
4 | /** @type {import('ts-jest').JestConfigWithTsJest} */
5 | module.exports = {
6 | preset: 'ts-jest',
7 | displayName: 'client',
8 | testEnvironment: 'jsdom',
9 | roots: ['/src/'],
10 | testMatch: ['**/*.test.ts?(x)'],
11 | transformIgnorePatterns: ['../../node_modules', '/node_modules'],
12 | modulePaths: [compilerOptions.baseUrl],
13 | moduleNameMapper: {
14 | uuid: require.resolve('uuid'), // ???
15 | ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }),
16 | '^react-native$': 'react-native-web',
17 | '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy'
18 | },
19 | setupFiles: ['jest-environment-jsdom'],
20 | setupFilesAfterEnv: ['/src/setupTests.ts'],
21 | transform: {
22 | '^.+\\.(js|jsx|mjs|cjs|ts|tsx)$': '/tools/jest/babelTransform.js',
23 | '^.+\\.css$': '/tools/jest/cssTransform.js',
24 | '^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)': '/tools/jest/fileTransform.js'
25 | },
26 | transformIgnorePatterns: [
27 | '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$',
28 | '^.+\\.module\\.(css|sass|scss)$'
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/index.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@mui/material/Box'
2 | import { Route, Routes } from 'react-router-dom'
3 | import Dashboard from './Dashboard'
4 | import Data from './Data'
5 | import Menu from './Menu'
6 | import Orders from './Orders'
7 | import Settings from './Settings/index'
8 | import Subscriptions from './Subscriptions'
9 | import Users from './Users'
10 |
11 | /**
12 | * Intent: App console look and feel
13 | * - Sticky menu and footer
14 | * - data grid
15 | * - home
16 | * - active users
17 | * - recent activity
18 | * - recent errors
19 | */
20 | export default function Admin(): JSX.Element {
21 | return (
22 |
23 |
24 |
31 |
32 | } />
33 | } />
34 | } />
35 | } />
36 | } />
37 | } />
38 |
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/workspaces/client/scripts/test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | // Do this as the first thing so that any code reading it knows the right env.
4 | process.env.BABEL_ENV = 'test'
5 | process.env.NODE_ENV = 'test'
6 | process.env.PUBLIC_URL = ''
7 |
8 | // Makes the script crash on unhandled rejections instead of silently
9 | // ignoring them. In the future, promise rejections that are not handled will
10 | // terminate the Node.js process with a non-zero exit code.
11 | process.on('unhandledRejection', err => {
12 | throw err
13 | })
14 |
15 | // Ensure environment variables are read.
16 | require('../tools/env')
17 |
18 | const jest = require('jest')
19 | const execSync = require('child_process').execSync
20 | let argv = process.argv.slice(2)
21 |
22 | function isInGitRepository() {
23 | try {
24 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' })
25 | return true
26 | } catch (e) {
27 | return false
28 | }
29 | }
30 |
31 | function isInMercurialRepository() {
32 | try {
33 | execSync('hg --cwd . root', { stdio: 'ignore' })
34 | return true
35 | } catch (e) {
36 | return false
37 | }
38 | }
39 |
40 | console.log('Directory: ', process.cwd())
41 | const hasSourceControl = isInGitRepository() || isInMercurialRepository()
42 | console.log('hasSourceControl: ', hasSourceControl)
43 | jest.run(argv)
44 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Dashboard/chart-data/total-order-month-line-chart.ts:
--------------------------------------------------------------------------------
1 | // ===========================|| DASHBOARD - TOTAL ORDER MONTH CHART ||=========================== //
2 |
3 | const chartData = {
4 | type: 'line',
5 | height: 90,
6 | options: {
7 | chart: {
8 | sparkline: {
9 | enabled: true
10 | }
11 | },
12 | dataLabels: {
13 | enabled: false
14 | },
15 | colors: ['#fff'],
16 | fill: {
17 | type: 'solid',
18 | opacity: 1
19 | },
20 | stroke: {
21 | curve: 'smooth',
22 | width: 3
23 | },
24 | yaxis: {
25 | min: 0,
26 | max: 100
27 | },
28 | tooltip: {
29 | theme: 'dark',
30 | fixed: {
31 | enabled: false
32 | },
33 | x: {
34 | show: false
35 | },
36 | y: {
37 | title: 'Total Order'
38 | },
39 | marker: {
40 | show: false
41 | }
42 | }
43 | },
44 | series: [
45 | {
46 | name: 'series1',
47 | data: [45, 66, 41, 89, 25, 44, 9, 54]
48 | }
49 | ]
50 | };
51 |
52 | export default chartData;
53 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Dashboard/chart-data/total-order-year-line-chart.ts:
--------------------------------------------------------------------------------
1 | // ===========================|| DASHBOARD - TOTAL ORDER YEAR CHART ||=========================== //
2 |
3 | const chartData = {
4 | type: 'line',
5 | height: 90,
6 | options: {
7 | chart: {
8 | sparkline: {
9 | enabled: true
10 | }
11 | },
12 | dataLabels: {
13 | enabled: false
14 | },
15 | colors: ['#fff'],
16 | fill: {
17 | type: 'solid',
18 | opacity: 1
19 | },
20 | stroke: {
21 | curve: 'smooth',
22 | width: 3
23 | },
24 | yaxis: {
25 | min: 0,
26 | max: 100
27 | },
28 | tooltip: {
29 | theme: 'dark',
30 | fixed: {
31 | enabled: false
32 | },
33 | x: {
34 | show: false
35 | },
36 | y: {
37 | title: 'Total Order'
38 | },
39 | marker: {
40 | show: false
41 | }
42 | }
43 | },
44 | series: [
45 | {
46 | name: 'series1',
47 | data: [35, 44, 9, 54, 45, 66, 41, 69]
48 | }
49 | ]
50 | };
51 |
52 | export default chartData;
53 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Users/UserOrders.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import { DataGrid, GridColDef } from '@mui/x-data-grid'
3 | import { Order, PagedResult } from '@lib'
4 | import { useGet } from '../../app'
5 |
6 | export function UserOrders() {
7 | const { data, isLoading, error } = useGet>(
8 | 'orders',
9 | `user/orders`,
10 | {},
11 | { limit: 100, page: 0 },
12 | )
13 | const columns: GridColDef[] = [
14 | {
15 | field: 'id',
16 | headerName: 'ID',
17 | width: 100,
18 | },
19 | {
20 | field: 'status',
21 | headerName: 'Status',
22 | width: 150,
23 | },
24 | {
25 | field: 'total',
26 | headerName: 'Total',
27 | width: 150,
28 | },
29 | {
30 | field: 'createdAt',
31 | headerName: 'Created At',
32 | width: 150,
33 | },
34 | {
35 | field: 'updatedAt',
36 | headerName: 'Updated At',
37 | width: 150,
38 | },
39 | ]
40 | return (
41 |
42 |
49 |
50 | )
51 | }
52 |
53 | export default UserOrders
54 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Routing.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Helmet } from 'react-helmet-async'
3 | import { Route, Routes } from 'react-router-dom'
4 | import { config } from '../../shared/config'
5 | import routes, { AppRoute } from '../../shared/routes'
6 | import AuthCheck from '../profile/AuthCheck'
7 | import CircularProgress from '@mui/material/CircularProgress'
8 |
9 | export const RouteElement = ({ route }: { route: AppRoute }) => (
10 |
11 |
12 | {`${route.title} - ${config.defaultTitle}`}
13 |
14 | }>
15 |
16 |
17 |
18 | )
19 |
20 | export default function Routing() {
21 | return (
22 |
23 | {routes.map(route => (
24 | } />
25 | ))}
26 | {routes
27 | .filter(r => !!r.params)
28 | .map(route =>
29 | route.params?.map(pathParam => (
30 | }
34 | />
35 | ))
36 | )}
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Card/Skeleton/TotalGrowthBarChart.tsx:
--------------------------------------------------------------------------------
1 | import Skeleton from '@mui/material/Skeleton'
2 | import { gridSpacing } from '../../../../shared/constant'
3 | import Card from '@mui/material/Card'
4 | import CardContent from '@mui/material/CardContent'
5 | import Grid from '@mui/material/Grid'
6 |
7 | const TotalGrowthBarChart = () => (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 |
36 | export default TotalGrowthBarChart
37 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/canvas/NameEdit.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useAppDispatch, useAppSelector } from '../../shared/store'
3 | import { actions } from './slice'
4 | import { styled } from '@mui/material/styles'
5 | import TextField from '@mui/material/TextField'
6 |
7 | const FieldStyled = styled(TextField)({
8 | position: 'absolute',
9 | top: '10%',
10 | left: '20%'
11 | })
12 |
13 | export default function NameEdit({
14 | inputRef,
15 | save
16 | }: {
17 | inputRef: React.RefObject
18 | save: () => void
19 | }) {
20 | const active = useAppSelector(state => state.canvas?.active)
21 | const dispatch = useAppDispatch()
22 | const onNameChange = React.useCallback(
23 | (e: { target: { value: string } }) => {
24 | dispatch(actions.patchActive({ name: e.target.value }))
25 | },
26 | [dispatch]
27 | )
28 | const onKeyUp = (e: React.KeyboardEvent) => {
29 | if (e.key === 'Enter') {
30 | save()
31 | }
32 | }
33 | return (
34 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@mui/material/Box'
2 | import Container from '@mui/material/Container'
3 | import React from 'react'
4 | import Grid from '@mui/material/Grid'
5 | import Typography from '@mui/material/Typography'
6 | import { TypographyProps } from '@mui/system'
7 | import github from '../home/images/github.svg'
8 | import Link from '@mui/material/Link'
9 |
10 | const Text = ({ children, ...rest }: TypographyProps & { children: React.ReactNode }) => (
11 |
12 | {children}
13 |
14 | )
15 |
16 | export default function Footer() {
17 | return (
18 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/profile/Orders.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import { DataGrid, GridColDef } from '@mui/x-data-grid'
3 | import { Order, PagedResult } from '@lib'
4 | import { useGet } from '../../features/app'
5 |
6 | export function Orders() {
7 | const { data, isLoading, error } = useGet>('ordercache', 'order', undefined, {
8 | limit: 100,
9 | page: 0,
10 | })
11 | const columns: GridColDef[] = [
12 | {
13 | field: 'id',
14 | headerName: 'ID',
15 | width: 100,
16 | },
17 | {
18 | field: 'status',
19 | headerName: 'Status',
20 | width: 150,
21 | },
22 | {
23 | field: 'total',
24 | headerName: 'Total',
25 | width: 150,
26 | },
27 | {
28 | field: 'createdAt',
29 | headerName: 'Created At',
30 | width: 150,
31 | },
32 | {
33 | field: 'updatedAt',
34 | headerName: 'Updated At',
35 | width: 150,
36 | },
37 | ]
38 | return (
39 |
40 | row[Object.keys(row)[0]]}
42 | rows={data?.items || []}
43 | columns={columns}
44 | pageSize={5}
45 | checkboxSelection
46 | disableSelectionOnClick
47 | />
48 |
49 | )
50 | }
51 |
52 | export default Orders
53 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/types/models/order.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes } from 'sequelize'
2 | import { addModel } from '../../db/util'
3 | import { Order, OrderItem } from '@lib'
4 |
5 | export const OrderModel = addModel({
6 | name: 'order',
7 | attributes: {
8 | orderId: {
9 | type: DataTypes.UUID,
10 | primaryKey: true,
11 | defaultValue: DataTypes.UUIDV4
12 | },
13 | userId: {
14 | type: DataTypes.UUID
15 | },
16 | status: {
17 | type: DataTypes.STRING
18 | },
19 | total: {
20 | type: DataTypes.DECIMAL(10, 2)
21 | }
22 | }
23 | })
24 |
25 | export const OrderItemModel = addModel({
26 | name: 'orderItem',
27 | attributes: {
28 | orderItemId: {
29 | type: DataTypes.UUID,
30 | primaryKey: true,
31 | defaultValue: DataTypes.UUIDV4
32 | },
33 | orderId: {
34 | type: DataTypes.UUID
35 | },
36 | drawingId: {
37 | type: DataTypes.UUID
38 | },
39 | productId: {
40 | type: DataTypes.STRING
41 | },
42 | priceId: {
43 | type: DataTypes.STRING
44 | },
45 | paid: {
46 | type: DataTypes.DECIMAL(10, 2)
47 | },
48 | quantity: {
49 | type: DataTypes.INTEGER
50 | },
51 | tokens: {
52 | type: DataTypes.INTEGER
53 | },
54 | type: {
55 | type: DataTypes.STRING
56 | }
57 | }
58 | })
59 |
--------------------------------------------------------------------------------
/workspaces/client/tools/react-intl-sync.js:
--------------------------------------------------------------------------------
1 | const globSync = require('glob/sync')
2 | const fs = require('fs')
3 | const path = require('path')
4 | const EXTRACT_FILE = 'extract.json'
5 | const DIR = path.resolve(__dirname, '../src/features/languages')
6 | const MESSAGES = require(`${DIR}/${EXTRACT_FILE}`)
7 |
8 | const seenIds = new Set()
9 | const extract = Object.keys(MESSAGES).reduce((acc, key) => {
10 | const { defaultMessage } = MESSAGES[key]
11 | const id = key
12 | if (seenIds.has(id)) {
13 | throw new Error(`Duplicate message descriptor ID: ${id}`)
14 | }
15 | seenIds.add(id)
16 | if (defaultMessage.includes('\n')) {
17 | throw new Error(`Default message from ${id} contains a line break: ${defaultMessage}`)
18 | }
19 | acc[key] = defaultMessage
20 | return acc
21 | }, {})
22 |
23 | const write = (localeFileName, data) =>
24 | fs.writeFileSync(`${DIR}/${localeFileName}`, JSON.stringify(data, null, 2))
25 |
26 | // if en does not exist just create that
27 | if (!fs.existsSync(`${DIR}/en.json`)) {
28 | write('en.json', extract)
29 | return
30 | }
31 |
32 | globSync(`${DIR}/*.json`).forEach(filename => {
33 | if (filename.includes(EXTRACT_FILE)) return
34 | const locale = filename.split('/').pop()
35 | const fileContent = JSON.parse(fs.readFileSync(filename, 'utf8'))
36 | const merged = { ...extract, ...fileContent }
37 | write(locale, merged)
38 | })
39 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/StyledSlider.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@mui/material/styles'
2 | import Slider, { SliderProps } from '@mui/material/Slider'
3 |
4 | const StyledSlider = styled(Slider)(() => ({
5 | color: '#7e57c2',
6 | height: 8,
7 | '& .MuiSlider-track': {
8 | border: 'none',
9 | },
10 | '& .MuiSlider-thumb': {
11 | transition: 'all 1s ease-in-out',
12 | height: 24,
13 | width: 24,
14 | backgroundColor: '#000',
15 | border: '2px solid currentColor',
16 | '&:focus, &:hover, &.Mui-active, &.Mui-focusVisible': {
17 | boxShadow: 'inherit',
18 | },
19 | '&:before': {
20 | display: 'none',
21 | },
22 | },
23 | '& .MuiSlider-valueLabel': {
24 | lineHeight: 1.2,
25 | fontSize: 12,
26 | background: 'unset',
27 | padding: 0,
28 | width: 32,
29 | height: 32,
30 | borderRadius: '50% 50% 50% 0',
31 | backgroundColor: '#7e57c2',
32 | transformOrigin: 'bottom left',
33 | transform: 'translate(50%, -100%) rotate(-45deg) scale(0)',
34 | '&:before': { display: 'none' },
35 | '&.MuiSlider-valueLabelOpen': {
36 | transform: 'translate(50%, -100%) rotate(-45deg) scale(1)',
37 | },
38 | '& > *': {
39 | transform: 'rotate(45deg)',
40 | },
41 | },
42 | }))
43 |
44 | export default function WrapFix(props: SliderProps) {
45 | return
46 | }
47 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/home/Subscribe.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, CardContent, Grid, TextField } from '@mui/material'
2 | import Box from '@mui/material/Box'
3 | import Typography from '@mui/material/Typography'
4 |
5 | export default function Subscribe() {
6 | return (
7 |
8 |
9 |
10 |
15 |
16 |
20 | Lorem ipsum dolor sit amet
21 |
22 |
23 |
24 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "npm",
6 | "script": "dev",
7 | "path": "workspaces/client",
8 | "group": "build",
9 | "label": "client.watch",
10 | "detail": "yarn client watch",
11 | "isBackground": true,
12 | "problemMatcher": [
13 | {
14 | "pattern": [
15 | {
16 | "regexp": ".",
17 | "file": 1,
18 | "location": 2,
19 | "message": 3
20 | }
21 | ],
22 | "background": {
23 | "activeOnStart": true,
24 | "beginsPattern": ".",
25 | "endsPattern": "."
26 | }
27 | }
28 | ]
29 | },
30 | {
31 | "type": "npm",
32 | "script": "docker",
33 | "group": "build",
34 | "label": "docker.start",
35 | "detail": "yarn docker",
36 | "isBackground": true,
37 | "problemMatcher": [
38 | {
39 | "pattern": [
40 | {
41 | "regexp": ".",
42 | "file": 1,
43 | "location": 2,
44 | "message": 3
45 | }
46 | ],
47 | "background": {
48 | "activeOnStart": true,
49 | "beginsPattern": ".",
50 | "endsPattern": "."
51 | }
52 | }
53 | ]
54 | }
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/db/check.ts:
--------------------------------------------------------------------------------
1 | import config from '../config'
2 | import logger from '../logger'
3 | import Connection from '.'
4 | import { checkMigrations } from './migrator'
5 | import { createDatabase } from './util'
6 |
7 | export async function checkDatabase(syncOverride?: boolean): Promise {
8 | if (!Connection.initialized) {
9 | logger.error('DB Connection not initialized')
10 | return false
11 | }
12 |
13 | const syncModels = syncOverride ? syncOverride : config.db.sync
14 |
15 | try {
16 | logger.info('Connecting to database...')
17 | config.db.models = Connection.entities.map(m => m.name)
18 | await Connection.db.authenticate()
19 | logger.info(
20 | `Database: models:
21 | ${Connection.entities.map(a => a.name).join(', ')}`
22 | )
23 | if (syncModels) {
24 | await Connection.db.sync({ alter: config.db.alter, force: config.db.force })
25 | }
26 | logger.info('Database: Connected')
27 | return true
28 | } catch (e: unknown) {
29 | const msg = (e as Error)?.message
30 | logger.error('Unable to connect to the database:', e)
31 | if (msg?.includes('does not exist')) {
32 | const result = await createDatabase()
33 | return result
34 | }
35 | if (msg?.includes('column')) {
36 | const result = await checkMigrations()
37 | return result
38 | }
39 | }
40 | return false
41 | }
42 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/app/ConfigProvider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import loadConfig from '../../shared/loadConfig'
3 | import { useAppSelector } from '../../shared/store'
4 | import MaintenancePage from '../../features/pages/Maintenance'
5 | import StartPage from '../../features/pages/Start'
6 | import { useLocation } from 'react-router-dom'
7 | import { hasRole } from '../../shared/auth'
8 | import { Config } from 'src/shared/config'
9 | import { useQuery } from 'react-query'
10 |
11 | export function ConfigProvider({ children }: { children: React.ReactElement }): JSX.Element {
12 | const location = useLocation()
13 | const loaded = useAppSelector(state => state.app.loaded)
14 | const ready = useAppSelector(state => state.app.ready)
15 | const token = useAppSelector(state => state.app.token)
16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
17 | const { data, isLoading } = useQuery('config', () => loadConfig())
18 | const maintenance = useAppSelector(state => state.app.settings?.system?.disable)
19 | const showStart = loaded && !ready
20 | const showMaintenance =
21 | maintenance && (!token || !hasRole('admin')) && location.pathname !== '/login'
22 |
23 | if (showStart) {
24 | return
25 | }
26 |
27 | if (showMaintenance) {
28 | return
29 | }
30 |
31 | return children
32 | }
33 |
34 | export default ConfigProvider
35 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/canvas/Toolbar.tsx:
--------------------------------------------------------------------------------
1 | import { Settings } from '@mui/icons-material'
2 | import { useAppDispatch } from '../../shared/store'
3 | import Color from './Color'
4 | import LineSize from './LineSize'
5 | import { actions } from './slice'
6 | import Box from '@mui/material/Box'
7 | import Fab from '@mui/material/Fab'
8 | import Stack from '@mui/material/Stack'
9 |
10 | export function Toolbar({
11 | newHandler,
12 | clearHandler,
13 | saveHandler
14 | }: {
15 | newHandler: () => void
16 | clearHandler: () => void
17 | saveHandler: () => void
18 | }) {
19 | const dispatch = useAppDispatch()
20 | const showDetails = () => dispatch(actions.patch({ showDetails: true }))
21 | return (
22 | <>
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | New
31 |
32 |
33 | Clear
34 |
35 |
36 | Save
37 |
38 |
39 |
40 |
41 | >
42 | )
43 | }
44 |
45 | export default Toolbar
46 |
--------------------------------------------------------------------------------
/.github/workflows/client-deploy-ghpages.yml:
--------------------------------------------------------------------------------
1 | name: 'Deploy: Client: Github Pages'
2 |
3 | on:
4 | push:
5 | branches: ['master']
6 | paths:
7 | - rem
8 |
9 | env:
10 | HOMEPAGE: ''
11 |
12 | jobs:
13 | publish:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [16.x]
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 |
27 | - name: Set Env ${{ github.repository }}
28 | run: |
29 | HOMEPAGE=${GITHUB_REPOSITORY#*/}
30 | echo "Found: ${HOMEPAGE}"
31 | echo "HOMEPAGE=${HOMEPAGE}" >> $GITHUB_ENV
32 |
33 | - name: package.json set ${{ env.HOMEPAGE }} as homepage for gh-pages
34 | uses: jaywcjlove/github-action-package@main
35 | with:
36 | path: 'workspaces/client/package.json'
37 | data: |
38 | {
39 | "homepage": "/${{ env.HOMEPAGE }}"
40 | }
41 |
42 | - name: Build
43 | run: |
44 | yarn install
45 | yarn workspace client build
46 |
47 | - name: Deploy
48 | uses: peaceiris/actions-gh-pages@v3
49 | with:
50 | github_token: ${{ secrets.GITHUB_TOKEN }}
51 | publish_dir: ./workspaces/client/build
52 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/AlertDialog.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@mui/material/Button'
2 | import Dialog from '@mui/material/Dialog'
3 | import DialogActions from '@mui/material/DialogActions'
4 | import DialogContent from '@mui/material/DialogContent'
5 | import DialogContentText from '@mui/material/DialogContentText'
6 | import DialogTitle from '@mui/material/DialogTitle'
7 |
8 | export interface ShowDialogProps {
9 | open: boolean
10 | title?: string
11 | message?: string
12 | payload?: unknown
13 | onConfirm?: () => void
14 | onCancel?: () => void
15 | alert?: boolean
16 | }
17 |
18 | export default function ConfirmDialog({
19 | message,
20 | title,
21 | open,
22 | onCancel,
23 | onConfirm,
24 | alert
25 | }: ShowDialogProps) {
26 | return (
27 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/app/App.tsx:
--------------------------------------------------------------------------------
1 | import { Provider } from 'react-redux'
2 | import { QueryClient, QueryClientProvider } from 'react-query'
3 | import { BrowserRouter } from 'react-router-dom'
4 | import { HelmetProvider } from 'react-helmet-async'
5 | import { store } from '../../shared/store'
6 | import { config } from '../../shared/config'
7 | import { MainLayout } from '../ui/MainLayout'
8 | import ConfigProvider from './ConfigProvider'
9 | import axios from 'axios'
10 | import ThemeSwitch from '../ui/Theme'
11 | import LanguageProvider from './LanguageProvider'
12 | import { firebaseAppInit } from 'src/shared/firebase'
13 |
14 | firebaseAppInit()
15 |
16 | const queryClient = new QueryClient({
17 | defaultOptions: { queries: { retry: true, cacheTime: 3000, staleTime: 3000 } }
18 | })
19 |
20 | axios.defaults.baseURL = config.backendUrl
21 |
22 | export default function App() {
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/workspaces/client/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | firebase-debug.log*
8 | firebase-debug.*.log*
9 |
10 | # Firebase cache
11 | .firebase/
12 |
13 | # Firebase config
14 |
15 | # Uncomment this if you'd like others to create their own Firebase project.
16 | # For a team working on the same Firebase project(s), it is recommended to leave
17 | # it commented so all members can deploy to the same project(s) in .firebaserc.
18 | # .firebaserc
19 |
20 | # Runtime data
21 | pids
22 | *.pid
23 | *.seed
24 | *.pid.lock
25 |
26 | # Directory for instrumented libs generated by jscoverage/JSCover
27 | lib-cov
28 |
29 | # Coverage directory used by tools like istanbul
30 | coverage
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # Bower dependency directory (https://bower.io/)
39 | bower_components
40 |
41 | # node-waf configuration
42 | .lock-wscript
43 |
44 | # Compiled binary addons (http://nodejs.org/api/addons.html)
45 | build/Release
46 |
47 | # Dependency directories
48 | node_modules/
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional REPL history
57 | .node_repl_history
58 |
59 | # Output of 'npm pack'
60 | *.tgz
61 |
62 | # Yarn Integrity file
63 | .yarn-integrity
64 |
65 | # dotenv environment variables file
66 | .env
67 |
--------------------------------------------------------------------------------
/workspaces/client/tools/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const camelcase = require('camelcase')
5 |
6 | // This is a custom Jest transformer turning file imports into filenames.
7 | // http://facebook.github.io/jest/docs/en/webpack.html
8 |
9 | module.exports = {
10 | process(src, filename) {
11 | const assetFilename = JSON.stringify(path.basename(filename))
12 |
13 | if (filename.match(/\.svg$/)) {
14 | // Based on how SVGR generates a component name:
15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
16 | const pascalCaseFilename = camelcase(path.parse(filename).name, {
17 | pascalCase: true
18 | })
19 | const componentName = `Svg${pascalCaseFilename}`
20 | return {
21 | code: `const React = require('react');
22 | module.exports = {
23 | __esModule: true,
24 | default: ${assetFilename},
25 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
26 | return {
27 | $$typeof: Symbol.for('react.element'),
28 | type: 'svg',
29 | ref: ref,
30 | key: null,
31 | props: Object.assign({}, props, {
32 | children: ${assetFilename}
33 | })
34 | };
35 | }),
36 | };`
37 | }
38 | }
39 |
40 | return { code: `module.exports = ${assetFilename};` }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/auth.ts:
--------------------------------------------------------------------------------
1 | import { User } from './user'
2 |
3 | export interface Jwt {
4 | [key: string]: unknown
5 | iss?: string | undefined
6 | sub?: string | undefined
7 | aud?: string | string[] | undefined
8 | exp?: number | undefined
9 | nbf?: number | undefined
10 | iat?: number | undefined
11 | jti?: string | undefined
12 | }
13 |
14 | export interface AppAccessToken extends Jwt {
15 | uid: string
16 | roles: string[]
17 | memberships: string[]
18 | claims: {
19 | [key: string]: unknown
20 | }
21 | }
22 |
23 | export interface IdentityToken extends Jwt {
24 | picture?: string | undefined
25 | email: string
26 | name: string
27 | given_name: string
28 | family_name: string
29 | }
30 |
31 | export interface oAuthError {
32 | error?: string
33 | error_description?: string
34 | status?: number
35 | }
36 |
37 | export interface oAuthResponse extends oAuthError {
38 | access_token?: string
39 | id_token?: string
40 | scope?: string
41 | expires_in?: number
42 | token_type?: string
43 | decoded?: IdentityToken
44 | user?: User
45 | }
46 |
47 | export interface oAuthRegistered extends oAuthError {
48 | _id?: string
49 | email?: string
50 | family_name?: string
51 | given_name?: string
52 | email_verified?: boolean
53 | }
54 |
55 | export type oAuthInputs = { [key: string]: string } & {
56 | email?: string
57 | password?: string
58 | idToken?: string
59 | firstName?: string
60 | lastName?: string
61 | uid?: string
62 | }
63 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/drawing.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from '.'
2 | import { User } from './user'
3 |
4 | export enum ActionType {
5 | Open = 0,
6 | Close = 1,
7 | Stroke = 2,
8 | }
9 |
10 | /**
11 | * Reducing space as much as possible
12 | *
13 | * c: color
14 | * w: width/size
15 | * ts: unix timestamp
16 | */
17 | // better space would be, mini serializer needed
18 | // JSON.stringify([...Object.values({ x, y, t, w, st, ts })])
19 | export interface DrawAction {
20 | t: ActionType
21 | x?: number
22 | y?: number
23 | c?: string
24 | w?: number
25 | ts?: number
26 | }
27 |
28 | export interface Drawing extends Entity {
29 | drawingId?: string
30 | userId?: string
31 | name: string
32 | history: DrawAction[]
33 | thumbnail?: string
34 | private?: boolean
35 | sell?: boolean
36 | price?: number
37 | hits?: number
38 | user?: User
39 | }
40 |
41 | // might be better to add up all open closes hmm
42 | export function getTimeSpent(d: Drawing): number {
43 | if (!d?.history || d.history?.length < 2) {
44 | return 0
45 | }
46 | const first = d.history[0].ts as number
47 | const last = d.history[d.history.length - 1].ts as number
48 | const millisecs = last - first
49 | return millisecs
50 | }
51 |
52 | export function getDuration(d: Drawing) {
53 | const secs = Math.round(getTimeSpent(d) / 1000)
54 | const mins = Math.round(secs / 60)
55 | const hours = Math.round(mins / 60)
56 | const rem = secs % 60
57 | return `${hours}h:${mins}m:${rem}s`
58 | }
59 |
--------------------------------------------------------------------------------
/workspaces/client/src/shared/store.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
2 | import { configureStore, ThunkAction, Action, Middleware } from '@reduxjs/toolkit'
3 | import appReducer from '../features/app/slice'
4 | import canvasReducer from '../features/canvas/slice'
5 | import adminReducer from '../features/admin/slice'
6 | import shopReducer from '../features/shop/slice'
7 | import { load } from 'redux-localstorage-simple'
8 |
9 | export const customMiddleware: Middleware = () => next => action => {
10 | const result = next(action)
11 | return result
12 | }
13 |
14 | export const reducers = {
15 | app: appReducer,
16 | canvas: canvasReducer,
17 | admin: adminReducer,
18 | shop: shopReducer
19 | }
20 |
21 | export const store = configureStore({
22 | reducer: reducers,
23 | preloadedState: load(),
24 | middleware: getDefaultMiddleware => getDefaultMiddleware().concat([customMiddleware])
25 | })
26 |
27 | export type AppDispatch = typeof store.dispatch
28 | export type RootState = ReturnType
29 | export type AppThunk = ThunkAction<
30 | ReturnType,
31 | RootState,
32 | unknown,
33 | Action
34 | >
35 |
36 | /**
37 | * Use instead of plain `useDispatch` */
38 | export const useAppDispatch = () => useDispatch()
39 |
40 | /**
41 | * Use instead of plain `useSelector` */
42 | export const useAppSelector: TypedUseSelectorHook = useSelector
43 |
44 | export const getStore = () => store
45 |
46 | export default store
47 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/canvas/slice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2 | import { Drawing } from '@lib'
3 | import { getDraft } from './helpers'
4 |
5 | export interface CanvasState {
6 | active: Drawing
7 | items: Drawing[]
8 | loaded?: boolean
9 | loading?: boolean
10 | size?: number
11 | color?: string
12 | style?: string
13 | showDetails?: boolean
14 | }
15 |
16 | const active = getDraft()
17 |
18 | const initialState: CanvasState = {
19 | active,
20 | items: [],
21 | }
22 |
23 | export const canvasSlice = createSlice({
24 | name: 'canvas',
25 | initialState,
26 | reducers: {
27 | patch: (state, action: PayloadAction>) => {
28 | return { ...state, ...action.payload }
29 | },
30 | patchActive: (state, action: PayloadAction>) => {
31 | return { ...state, active: { ...state.active, ...action.payload } }
32 | },
33 | onSave: (state, action: PayloadAction) => {
34 | state.active = action.payload
35 | const existing = state.items.find(item => item.drawingId === action.payload.drawingId)
36 | if (existing) {
37 | state.items = state.items.map(item =>
38 | item.drawingId === action.payload.drawingId ? action.payload : item,
39 | )
40 | } else {
41 | state.items.push(action.payload)
42 | }
43 | },
44 | },
45 | })
46 |
47 | export const { patch } = canvasSlice.actions
48 |
49 | export const actions = canvasSlice.actions
50 |
51 | export default canvasSlice.reducer
52 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/shop/slice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
2 | import {
3 | Wallet,
4 | type Address,
5 | type Cart,
6 | type Drawing,
7 | type Order,
8 | type PaymentMethod,
9 | type Price,
10 | type Product,
11 | type Subscription
12 | } from '@lib'
13 |
14 | export interface ShopState {
15 | products?: Product[]
16 | loaded?: boolean
17 | items: Cart[]
18 | wallet?: Wallet
19 | orders: Order[]
20 | subscriptions?: Subscription[]
21 | activeSubscription?: Subscription
22 | selectedSubscriptionProduct?: Partial
23 | addresses?: Address[]
24 | paymentMethods?: PaymentMethod[]
25 | showCart?: boolean
26 | showCheckout?: boolean
27 | receipt?: Order
28 | activeItem?: Drawing
29 | shippingAddressId?: string
30 | billingAddressId?: string
31 | paymentMethodId?: string
32 | activeStep: number
33 | steps: Record
34 | }
35 |
36 | export const initialState: ShopState = {
37 | items: [],
38 | orders: [],
39 | activeStep: 0,
40 | steps: {}
41 | }
42 |
43 | export const shopSlice = createSlice({
44 | name: 'shop',
45 | initialState,
46 | reducers: {
47 | patch(state, action: PayloadAction>) {
48 | return { ...state, ...action.payload }
49 | },
50 | stepStatus(state, payload: PayloadAction>) {
51 | state.steps = { ...state.steps, ...payload.payload }
52 | }
53 | }
54 | })
55 |
56 | export const { patch, stepStatus } = shopSlice.actions
57 | export const actions = shopSlice.actions
58 |
59 | export default shopSlice.reducer
60 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/auth/auth0.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosResponse } from 'axios'
2 | import config from '../config'
3 | import { oAuthInputs, oAuthResponse } from '@lib'
4 |
5 | export async function auth0Register(payload: oAuthInputs): Promise {
6 | try {
7 | const response = await axios.post(`${config.auth?.baseUrl}/dbconnections/signup`, {
8 | connection: 'Username-Password-Authentication',
9 | client_id: config.auth?.clientId,
10 | email: payload.email,
11 | password: payload.password,
12 | user_metadata: {
13 | id: payload.uid
14 | }
15 | })
16 | return response.data
17 | } catch (err) {
18 | const error = err as Error & { response: AxiosResponse }
19 | return {
20 | error: error.response?.data?.name ?? error.message,
21 | error_description: error.response?.data?.description ?? error.message
22 | }
23 | }
24 | }
25 |
26 | export async function auth0Login({ email, password }: oAuthInputs): Promise {
27 | try {
28 | const response = await axios.post(`${config.auth?.baseUrl}/oauth/token`, {
29 | client_id: config.auth?.clientId,
30 | client_secret: config.auth?.clientSecret,
31 | audience: `${config.auth?.baseUrl}/api/v2/`,
32 | grant_type: 'password',
33 | username: email,
34 | password
35 | })
36 | return response.data
37 | } catch (err) {
38 | const error = err as Error & { response: AxiosResponse }
39 | return {
40 | error: error.response?.data?.error || error.message,
41 | error_description: error.response?.data?.message || error.message
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Notifications.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useAppDispatch, useAppSelector } from '../../shared/store'
3 | import { AppNotification, patch } from '../app'
4 | import Snackbar from '@mui/material/Snackbar'
5 | import Alert from '@mui/material/Alert'
6 |
7 | export default function Notifications() {
8 | const notifications = useAppSelector(store => store.app.notifications)
9 | const dispatch = useAppDispatch()
10 | const [open, setOpen] = React.useState(false)
11 | const [message, setMessage] = React.useState(null)
12 |
13 | const update = React.useCallback(
14 | (notifications: AppNotification[]) => {
15 | dispatch(patch({ notifications }))
16 | },
17 | [dispatch]
18 | )
19 |
20 | const close = React.useCallback(() => {
21 | setOpen(false)
22 | }, [])
23 |
24 | React.useEffect(() => {
25 | if (notifications.length > 0 && !message) {
26 | setMessage({ ...notifications[0] })
27 | update(notifications.slice(1))
28 | setOpen(true)
29 | } else if (notifications.length && message && open) {
30 | setOpen(false)
31 | }
32 | }, [dispatch, message, notifications, open, update])
33 |
34 | return (
35 | setMessage(null) }}
40 | anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
41 | autoHideDuration={3000}
42 | >
43 |
44 | {message?.message}
45 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/canvas/Color.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Backspace, Check } from '@mui/icons-material'
3 | import { useAppDispatch, useAppSelector } from '../../shared/store'
4 | import { actions } from './slice'
5 | import Box, { BoxProps } from '@mui/material/Box'
6 | import Stack from '@mui/material/Stack'
7 | import Fab from '@mui/material/Fab'
8 |
9 | const colors = ['yellow', 'red', 'blue', 'green', 'black']
10 |
11 | export default function Color(props: BoxProps) {
12 | const dispatch = useAppDispatch()
13 | const activeColor = useAppSelector(state => state.canvas.color)
14 | const [prev, setPrev] = React.useState()
15 | const isActive = (c: string) => activeColor === c
16 | const setColor = (requested: string) => {
17 | const color = requested === 'transparent' && activeColor === 'transparent' ? prev : requested
18 | dispatch(actions.patch({ color }))
19 | setPrev(activeColor)
20 | }
21 |
22 | return (
23 |
24 |
25 | {colors.map(c => (
26 | setColor(c)}
32 | >
33 | {isActive(c) && }
34 |
35 | ))}
36 |
37 | setColor('transparent')}>
38 |
44 |
45 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/workspaces/client/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | // eslint-disable-next-line @typescript-eslint/triple-slash-reference
3 | ///
4 | ///
5 |
6 | declare namespace NodeJS {
7 | interface ProcessEnv {
8 | readonly NODE_ENV: 'development' | 'production' | 'test'
9 | readonly PUBLIC_URL: string
10 | }
11 | }
12 |
13 | declare module '*.avif' {
14 | const src: string
15 | export default src
16 | }
17 |
18 | declare module '*.bmp' {
19 | const src: string
20 | export default src
21 | }
22 |
23 | declare module '*.gif' {
24 | const src: string
25 | export default src
26 | }
27 |
28 | declare module '*.jpg' {
29 | const src: string
30 | export default src
31 | }
32 |
33 | declare module '*.jpeg' {
34 | const src: string
35 | export default src
36 | }
37 |
38 | declare module '*.png' {
39 | const src: string
40 | export default src
41 | }
42 |
43 | declare module '*.webp' {
44 | const src: string
45 | export default src
46 | }
47 |
48 | declare module '*.svg' {
49 | import * as React from 'react'
50 |
51 | export const ReactComponent: React.FunctionComponent<
52 | React.SVGProps & { title?: string }
53 | >
54 |
55 | const src: string
56 | export default src
57 | }
58 |
59 | declare module '*.module.css' {
60 | const classes: { readonly [key: string]: string }
61 | export default classes
62 | }
63 |
64 | declare module '*.module.scss' {
65 | const classes: { readonly [key: string]: string }
66 | export default classes
67 | }
68 |
69 | declare module '*.module.sass' {
70 | const classes: { readonly [key: string]: string }
71 | export default classes
72 | }
73 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Card/Skeleton/ProductPlaceholder.tsx:
--------------------------------------------------------------------------------
1 | import Skeleton from '@mui/material/Skeleton'
2 | import MainCard from '..'
3 | import CardContent from '@mui/material/CardContent'
4 | import Grid from '@mui/material/Grid'
5 | import Stack from '@mui/material/Stack'
6 |
7 | const ProductPlaceholder = () => (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 |
42 | export default ProductPlaceholder
43 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/home/images/ts.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/deploy.md:
--------------------------------------------------------------------------------
1 |
2 | ## Github Actions Setup for Google Cloud
3 |
4 | *Automate
5 |
6 | Workload Identity provides keyless federation for external resources like GKE and Github Actions
7 | https://github.com/google-github-actions/auth#setup
8 |
9 | gcloud iam workload-identity-pools create "default-pool" \
10 | --project="mstream-368503" \
11 | --location="global" \
12 | --display-name="Default Pool"
13 |
14 | gcloud iam workload-identity-pools providers create-oidc "github-provider" \
15 | --project="mstream-368503" \
16 | --location="global" \
17 | --workload-identity-pool="default-pool" \
18 | --display-name="Github Provider" \
19 | --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.aud=assertion.aud,attribute.repository=assertion.repository" \
20 | --issuer-uri="https://token.actions.githubusercontent.com"
21 |
22 | gcloud iam service-accounts create "github" \
23 | --project="mstream-368503" \
24 | --description="Github Actions" \
25 | --display-name="Github Actions"
26 |
27 | gcloud projects add-iam-policy-binding mstream-368503 \
28 | --member="serviceAccount:github@mstream-368503.iam.gserviceaccount.com" \
29 | --role="editor"
30 |
31 | gcloud iam service-accounts add-iam-policy-binding "github@mstream-368503.iam.gserviceaccount.com" \
32 | --project="mstream-368503" \
33 | --role="roles/iam.workloadIdentityUser" \
34 | --member="principalSet://iam.googleapis.com/projects/364055912546/locations/global/workloadIdentityPools/default-pool/*"
35 |
36 | gcloud iam workload-identity-pools providers describe "github-provider" \
37 | --project="mstream-368503" \
38 | --location="global" \
39 | --workload-identity-pool="default-pool" \
40 | --format="value(name)"
41 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-google-server.yml:
--------------------------------------------------------------------------------
1 | name: 'Deploy: Google Cloud - Server CloudRun'
2 | on:
3 | push:
4 | branches: ['master']
5 | paths:
6 | - 'workspaces/server/**'
7 | - '.github/workflows/deploy-google-server.yml'
8 | env:
9 | VM_NAME: 'drawserver'
10 | PROJECT: 'mstream-368503'
11 | PROJECT_NO: 364055912546
12 | ZONE: 'us-central1-a'
13 | REGISTRY: 'gcr.io'
14 | IMAGE_PATH: 'gcr.io/mstream-368503'
15 |
16 | jobs:
17 | server_deploy:
18 | runs-on: 'ubuntu-latest'
19 | permissions:
20 | contents: 'read'
21 | id-token: 'write'
22 | steps:
23 | - uses: 'actions/checkout@v3'
24 |
25 | - uses: actions/setup-node@v3
26 | with:
27 | node-version: 18
28 | cache: 'yarn'
29 |
30 | - name: 'auth'
31 | uses: 'google-github-actions/auth@v1'
32 | with:
33 | workload_identity_provider: 'projects/364055912546/locations/global/workloadIdentityPools/default-pool/providers/github-provider'
34 | service_account: 'github@mstream-368503.iam.gserviceaccount.com'
35 |
36 | - name: Set up Cloud SDK
37 | uses: google-github-actions/setup-gcloud@v1
38 |
39 | - name: 'install'
40 | run: yarn install --immutable
41 |
42 | - name: 'tests'
43 | run: |
44 | yarn workspace server test
45 |
46 | - id: cloudrun
47 | uses: 'google-github-actions/deploy-cloudrun@v1'
48 | with:
49 | service: 'server'
50 | source: 'workspaces/server/'
51 | secrets: |
52 | DB_URL=DB_URL:latest
53 | env_vars: |
54 | COMMIT_DATE=${{ github.event.head_commit.timestamp }}
55 |
56 | - name: 'Check output url'
57 | run: 'curl "${{ steps.cloudrun.outputs.url }}"'
58 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Dashboard/BajajAreaChartCard.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | import useTheme from '@mui/material/styles/useTheme'
4 | import ApexCharts from 'apexcharts'
5 | import Chart from 'react-apexcharts'
6 | import chartData from './chart-data/bajaj-area-chart'
7 | import Card from '@mui/material/Card'
8 | import Grid from '@mui/material/Grid'
9 | import Typography from '@mui/material/Typography'
10 |
11 | const BajajAreaChartCard = () => {
12 | const theme = useTheme()
13 |
14 | useEffect(() => {
15 | const newSupportChart = {
16 | ...chartData.options,
17 | tooltip: {
18 | theme: theme.palette.mode
19 | }
20 | }
21 | ApexCharts.exec(`support-chart`, 'updateOptions', newSupportChart)
22 | }, [theme.palette.mode])
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 | Bajaj Finery
32 |
33 |
34 |
35 |
36 | $1839.00
37 |
38 |
39 |
40 |
41 |
42 |
43 | 10% Profit
44 |
45 |
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | export default BajajAreaChartCard
53 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/canvas/worker.ts:
--------------------------------------------------------------------------------
1 | import { ActionType, DrawAction } from '@lib'
2 | import { createOffscreen } from './helpers'
3 |
4 | export type WorkMessage = {
5 | buffer: DrawAction[]
6 | width: number
7 | height: number
8 | dpr: number
9 | stopAt?: number
10 | stream?: boolean
11 | }
12 |
13 | const w = self as Window & typeof globalThis
14 |
15 | function send(background: OffscreenCanvasRenderingContext2D, width: number, height: number) {
16 | try {
17 | const data = background.getImageData(0, 0, width, height)
18 | // eslint-disable-next-line
19 | w?.postMessage(data)
20 | } catch (err: unknown) {
21 | // eslint-disable-next-line no-console
22 | console.error(err)
23 | }
24 | }
25 |
26 | function processHistory({ buffer, width, height, dpr, stream, stopAt }: WorkMessage) {
27 | const background = createOffscreen(width, height, dpr)
28 | if (!background) {
29 | return
30 | }
31 |
32 | let i = 0
33 | for (const { t, x, y, c, w } of buffer) {
34 | if (c) {
35 | background.strokeStyle = c
36 | }
37 | if (w) {
38 | background.lineWidth = w
39 | }
40 | if (t === ActionType.Open) {
41 | background.beginPath()
42 | }
43 | if ([ActionType.Open, ActionType.Stroke].includes(t)) {
44 | background.lineTo(x as number, y as number)
45 | background.stroke()
46 | }
47 | if (t === ActionType.Close) {
48 | background.closePath()
49 | if (stream) {
50 | send(background, width, height)
51 | }
52 | }
53 | if (stopAt === i) {
54 | return
55 | }
56 | i += 1
57 | }
58 | send(background, width, height)
59 | }
60 |
61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
62 | w.onmessage = ({ data }: { data: WorkMessage; ev?: MessageEvent }) => {
63 | processHistory(data)
64 | }
65 |
66 | export {}
67 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/home/HeroSection.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Link } from 'react-router-dom'
4 | import { Paths } from '../../shared/routes'
5 | import { FormattedMessage, defineMessages } from 'react-intl'
6 | import Box from '@mui/material/Box'
7 | import Typography from '@mui/material/Typography'
8 | import Button from '@mui/material/Button'
9 | import { Grid } from '@mui/material'
10 | import styled from '@mui/system/styled'
11 |
12 | const messages = defineMessages({
13 | buttonCaption: {
14 | id: 'home.button',
15 | description: 'Hero Button Caption',
16 | defaultMessage: 'Take it for a spin'
17 | }
18 | })
19 |
20 | const StyledTypography = styled(Typography)({
21 | transform:
22 | 'translate3d(0px, 0px, 0px) scale3d(1, 1, 1) rotateX(0deg) rotateY(0deg) rotateZ(0deg) skew(0deg, 0deg)',
23 | opacity: 1,
24 | transformStyle: 'preserve-3d',
25 | margin: '.5rem 0 1.5rem 0',
26 | fontWeight: 700,
27 | letterSpacing: 0
28 | })
29 |
30 | export default function HeroSection({
31 | subtitle = 'Easy to use template for web projects',
32 | caption = ,
33 | children
34 | }: {
35 | subtitle?: string
36 | caption?: React.ReactNode
37 | children?: React.ReactNode
38 | }) {
39 | return (
40 |
41 |
42 |
43 | {subtitle}
44 |
45 |
58 | {children}
59 |
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Dashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | import EarningCard from './EarningCard'
4 | import PopularCard from './PopularCard'
5 | import TotalOrderLineChartCard from './TotalOrderLineChartCard'
6 | import TotalIncomeDarkCard from './TotalIncomeDarkCard'
7 | import TotalIncomeLightCard from './TotalIncomeLightCard'
8 | import TotalGrowthBarChart from './TotalGrowthBarChart'
9 | import Grid from '@mui/material/Grid'
10 |
11 | const gridSpacing = 3
12 |
13 | const Dashboard = () => {
14 | const [isLoading, setLoading] = useState(true)
15 | useEffect(() => {
16 | setLoading(false)
17 | }, [])
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | )
53 | }
54 |
55 | export default Dashboard
56 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/trace.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import express from 'express'
3 | import axios from 'axios'
4 | import { config } from './config'
5 | import { decodeToken } from './auth'
6 | import { getRoutesFromApp } from './server'
7 | import logger from './logger'
8 |
9 | export function endpointTracingMiddleware(
10 | req: express.Request,
11 | res: express.Response,
12 | next: express.NextFunction
13 | ) {
14 | const methods = ['POST', 'PUT', 'PATCH', 'DELETE']
15 | const endpoints = ['/cart']
16 | if (!config.trace || !endpoints.includes(req.originalUrl) || !methods.includes(req.method)) {
17 | return next()
18 | }
19 | const token = req.headers.authorization?.replace('Bearer ', '') || ''
20 | if (config.auth.trace) {
21 | console.log('Token: ', token)
22 | }
23 | const decoded = decodeToken(token)
24 | console.log('\x1b[34m%s\x1b[0m', '******** INBOUND TRACE ** ')
25 | console.table({
26 | method: req.method,
27 | endpoint: req.originalUrl,
28 | tokenOk: !!decoded,
29 | userId: decoded?.uid
30 | })
31 | if (req.body) {
32 | console.table(req.body)
33 | }
34 | next()
35 | }
36 |
37 | export function activateAxiosTrace() {
38 | axios.interceptors.request.use(req => {
39 | // orange console log
40 | console.log(
41 | '\x1b[33m%s\x1b[0m',
42 | '> OUTBOUND TRACE ** ',
43 | req.method?.toUpperCase() || 'Request',
44 | req.url,
45 | config.trace ? req.data : ''
46 | )
47 | return req
48 | })
49 |
50 | axios.interceptors.response.use(req => {
51 | console.log('> Response:', req.status, req.statusText, config.trace ? req.data : '')
52 | return req
53 | })
54 | }
55 |
56 | export function printRouteSummary(app: express.Application) {
57 | if (!config.trace) {
58 | return
59 | }
60 | logger.info('******** ROUTE SUMMARY ********')
61 | const report = getRoutesFromApp(app).filter(r => r.from !== 'model-api')
62 | console.log(report)
63 | }
64 |
65 | export default logger
66 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/app/slice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2 | import { ClientSettings } from '@lib'
3 | import { AppUser, getPersistedAuthFromStorage } from '../../shared/auth'
4 | import { ThemeState } from '../ui/Theme/getTheme'
5 | import { AppNotification, NotificationSeverity } from './types'
6 |
7 | export interface AppState {
8 | user?: AppUser
9 | token?: string
10 | darkMode: boolean
11 | ui?: ThemeState
12 | notifications: AppNotification[]
13 | drawerLeftOpen?: boolean
14 | drawerRightOpen?: boolean
15 | loading?: boolean
16 | loaded?: boolean
17 | dialog?: string
18 | settings?: ClientSettings
19 | deviceId?: string
20 | ready: boolean
21 | locale: string
22 | }
23 |
24 | const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
25 | const defaultState: AppState = {
26 | darkMode: !!prefersDark,
27 | notifications: [],
28 | ready: false,
29 | locale: 'en',
30 | }
31 |
32 | const persistedAuth = getPersistedAuthFromStorage()
33 | const initialState = {
34 | ...defaultState,
35 | token: persistedAuth?.token,
36 | user: persistedAuth?.user,
37 | }
38 |
39 | const slice = createSlice({
40 | name: 'app',
41 | initialState,
42 | reducers: {
43 | patch: (state, action: PayloadAction>) => {
44 | return { ...state, ...action.payload }
45 | },
46 | notify: (state, action: PayloadAction) => {
47 | state.notifications.push({
48 | message: action.payload,
49 | id: new Date().getTime().toString(),
50 | severity: NotificationSeverity.info,
51 | })
52 | },
53 | notifyError: (state, action: PayloadAction) => {
54 | state.notifications.push({
55 | message: action.payload,
56 | id: new Date().getTime().toString(),
57 | severity: NotificationSeverity.error,
58 | })
59 | },
60 | },
61 | })
62 |
63 | export const { patch, notify, notifyError } = slice.actions
64 |
65 | export default slice.reducer
66 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/types/models/user.ts:
--------------------------------------------------------------------------------
1 | import { Address, User, UserActive } from '@lib'
2 | import { DataTypes } from 'sequelize'
3 | import { addModel } from '../../db/util'
4 |
5 | export const UserAttributes = {
6 | userId: {
7 | type: DataTypes.UUID,
8 | primaryKey: true,
9 | defaultValue: DataTypes.UUIDV4
10 | },
11 | firstName: {
12 | type: DataTypes.STRING
13 | },
14 | lastName: {
15 | type: DataTypes.STRING
16 | },
17 | email: {
18 | type: DataTypes.STRING
19 | },
20 | picture: {
21 | type: DataTypes.STRING
22 | },
23 | roles: {
24 | type: DataTypes.ARRAY(DataTypes.STRING)
25 | }
26 | }
27 |
28 | export const UserModel = addModel({ name: 'user', attributes: UserAttributes })
29 |
30 | export const UserActiveModel = addModel({
31 | name: 'user_active',
32 | attributes: {
33 | socketId: {
34 | type: DataTypes.STRING,
35 | primaryKey: true
36 | },
37 | userId: {
38 | type: DataTypes.UUID
39 | },
40 | ip: {
41 | type: DataTypes.STRING
42 | },
43 | userAgent: {
44 | type: DataTypes.STRING
45 | }
46 | }
47 | })
48 |
49 | export const AddressModel = addModel({
50 | name: 'address',
51 | attributes: {
52 | addressId: {
53 | type: DataTypes.UUID,
54 | primaryKey: true,
55 | defaultValue: DataTypes.UUIDV4
56 | },
57 | userId: {
58 | type: DataTypes.UUID
59 | },
60 | name: {
61 | type: DataTypes.STRING
62 | },
63 | address1: {
64 | type: DataTypes.STRING
65 | },
66 | address2: {
67 | type: DataTypes.STRING
68 | },
69 | city: {
70 | type: DataTypes.STRING
71 | },
72 | state: {
73 | type: DataTypes.STRING
74 | },
75 | zip: {
76 | type: DataTypes.STRING
77 | },
78 | country: {
79 | type: DataTypes.STRING
80 | },
81 | phone: {
82 | type: DataTypes.STRING
83 | },
84 | favorite: {
85 | type: DataTypes.BOOLEAN
86 | }
87 | }
88 | })
89 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/home/Logos.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@mui/system'
2 | import react from './images/react.svg'
3 | import ts from './images/ts.svg'
4 | import redux from './images/redux.svg'
5 | import query from './images/query.svg'
6 | import sequelize from './images/sequelize.svg'
7 | import nodejs from './images/nodejs.svg'
8 | import Box from '@mui/material/Box'
9 | import Typography from '@mui/material/Typography'
10 | import Grid from '@mui/material/Grid'
11 | import Card from '@mui/material/Card'
12 |
13 | import CardContent from '@mui/material/CardContent'
14 |
15 | const h = 80
16 | const w = 80
17 |
18 | const Logo = styled('img')({
19 | height: h,
20 | width: w
21 | })
22 |
23 | const logos = [
24 | ['TypeScript', ts],
25 | ['React', react],
26 | ['React Query', query],
27 | ['Redux Toolkit', redux],
28 | ['Sequelize', sequelize],
29 | ['NodeJS', nodejs]
30 | ]
31 |
32 | const StyledCard = styled(Card)({
33 | borderRadius: '14px',
34 | transition: 'all .2s ease-in-out',
35 | ':hover': {
36 | transform: 'scale(0.97)'
37 | }
38 | })
39 |
40 | export default function Logos() {
41 | return (
42 |
43 |
44 | Frontend and Backend Monorepo
45 |
46 |
47 | {logos.map((l, index) => (
48 |
49 |
50 |
51 |
52 | {l[0]}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | ))}
61 |
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Card/SubCard.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from '@mui/material/styles'
2 | import { CardProps } from './'
3 | import Card from '@mui/material/Card'
4 | import CardContent from '@mui/material/CardContent'
5 | import Divider from '@mui/material/Divider'
6 | import CardHeader from '@mui/material/CardHeader'
7 | import Typography from '@mui/material/Typography'
8 |
9 | export function SubCard(
10 | {
11 | children,
12 | content,
13 | contentClass,
14 | darkTitle,
15 | secondary,
16 | sx = {},
17 | contentSX = {},
18 | title,
19 | ...others
20 | }: CardProps,
21 | ref?: React.MutableRefObject
22 | ) {
23 | const theme = useTheme()
24 |
25 | return (
26 |
38 | {/* card header and action */}
39 | {!darkTitle && title && (
40 | {title}}
43 | action={secondary}
44 | />
45 | )}
46 | {darkTitle && title && (
47 | {title}}
50 | action={secondary}
51 | />
52 | )}
53 |
54 | {/* content & header divider */}
55 | {title && (
56 |
62 | )}
63 |
64 | {/* card content */}
65 | {content && (
66 |
67 | {children}
68 |
69 | )}
70 | {!content && children}
71 |
72 | )
73 | }
74 |
75 | export default SubCard
76 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/extended/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from '@mui/material/styles'
2 | import MuiAvatar from '@mui/material/Avatar'
3 |
4 | export interface AvatarProps {
5 | className?: string
6 | color?: string
7 | outline?: boolean
8 | size?: string
9 | sx?: object
10 | alt?: string
11 | href?: string
12 | target?: string
13 | children?: React.ReactNode
14 | component?: React.ElementType>
15 | }
16 |
17 | const Avatar = ({ color, outline, size, sx, ...others }: AvatarProps) => {
18 | const theme = useTheme()
19 |
20 | const colorSX = color &&
21 | !outline && { color: theme.palette.background.paper, bgcolor: `${color}.main` }
22 | const outlineSX = outline && {
23 | color: color ? `${color}.main` : `primary.main`,
24 | bgcolor: theme.palette.background.paper,
25 | border: '2px solid',
26 | borderColor: color ? `${color}.main` : `primary.main`,
27 | }
28 | let sizeSX = {}
29 | switch (size) {
30 | case 'badge':
31 | sizeSX = {
32 | width: theme.spacing(3.5),
33 | height: theme.spacing(3.5),
34 | }
35 | break
36 | case 'xs':
37 | sizeSX = {
38 | width: theme.spacing(4.25),
39 | height: theme.spacing(4.25),
40 | }
41 | break
42 | case 'sm':
43 | sizeSX = {
44 | width: theme.spacing(5),
45 | height: theme.spacing(5),
46 | }
47 | break
48 | case 'lg':
49 | sizeSX = {
50 | width: theme.spacing(9),
51 | height: theme.spacing(9),
52 | }
53 | break
54 | case 'xl':
55 | sizeSX = {
56 | width: theme.spacing(10.25),
57 | height: theme.spacing(10.25),
58 | }
59 | break
60 | case 'md':
61 | sizeSX = {
62 | width: theme.spacing(7.5),
63 | height: theme.spacing(7.5),
64 | }
65 | break
66 | default:
67 | sizeSX = {}
68 | }
69 |
70 | return
71 | }
72 |
73 | export default Avatar
74 |
--------------------------------------------------------------------------------
/workspaces/client/tools/getHttpsConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const path = require('path')
5 | const crypto = require('crypto')
6 | const chalk = require('react-dev-utils/chalk')
7 | const paths = require('./paths')
8 |
9 | // Ensure the certificate and key provided are valid and if not
10 | // throw an easy to debug error
11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
12 | let encrypted
13 | try {
14 | // publicEncrypt will throw an error with an invalid cert
15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test'))
16 | } catch (err) {
17 | throw new Error(`The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`)
18 | }
19 |
20 | try {
21 | // privateDecrypt will throw an error with an invalid key
22 | crypto.privateDecrypt(key, encrypted)
23 | } catch (err) {
24 | throw new Error(`The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${err.message}`)
25 | }
26 | }
27 |
28 | // Read file and throw an error if it doesn't exist
29 | function readEnvFile(file, type) {
30 | if (!fs.existsSync(file)) {
31 | throw new Error(
32 | `You specified ${chalk.cyan(type)} in your env, but the file "${chalk.yellow(
33 | file,
34 | )}" can't be found.`,
35 | )
36 | }
37 | return fs.readFileSync(file)
38 | }
39 |
40 | // Get the https config
41 | // Return cert files if provided in env, otherwise just true or false
42 | function getHttpsConfig() {
43 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env
44 | const isHttps = HTTPS === 'true'
45 |
46 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
47 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE)
48 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE)
49 | const config = {
50 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
51 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
52 | }
53 |
54 | validateKeyAndCerts({ ...config, keyFile, crtFile })
55 | return config
56 | }
57 | return isHttps
58 | }
59 |
60 | module.exports = getHttpsConfig
61 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/shop/OrderAddress.tsx:
--------------------------------------------------------------------------------
1 | import { useAppSelector } from 'src/shared/store'
2 | import { ExpandMore } from '@mui/icons-material'
3 | import Accordion from '@mui/material/Accordion'
4 | import AccordionSummary from '@mui/material/AccordionSummary'
5 | import Typography from '@mui/material/Typography'
6 | import AccordionDetails from '@mui/material/AccordionDetails'
7 | import Grid from '@mui/material/Grid'
8 |
9 | export default function OrderAddress() {
10 | const enableShipping = useAppSelector(state => state.app.settings?.system?.enableShippingAddress)
11 | const shippingAddressId = useAppSelector(state => state.shop.shippingAddressId)
12 | const addresses = useAppSelector(state => state.shop.addresses)
13 | const shippingAddress = addresses?.find(a => a.addressId === shippingAddressId)
14 |
15 | if (!enableShipping) {
16 | return null
17 | }
18 |
19 | return (
20 |
21 | } sx={{ borderRadius: '16px' }}>
22 | Shipping Address
23 |
27 | {shippingAddress?.address1}
28 |
29 |
30 |
31 |
32 |
33 | {shippingAddress?.name}
34 |
35 |
36 | {shippingAddress?.address1}
37 |
38 |
39 | {shippingAddress?.address2}
40 |
41 |
42 |
43 | {shippingAddress?.city}, {shippingAddress?.state} {shippingAddress?.zip}{' '}
44 | {shippingAddress?.country}
45 |
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/workspaces/client/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Drawspace - Fullstack Canvas Drawing App
2 |
3 | ## TypeScript App Project Template
4 |
5 | [](https://ruyd.github.io/fullstack-monorepo)
6 |
7 | [](https://api.drawspace.app/docs)
8 |
9 | [](https://drawspace.app)
10 |
11 |
12 | ### Best Practices Template 🙌
13 |
14 | [](https://raw.githubusercontent.com/ruyd/fullstack-monorepo/master/docs/images/4Pane.png)
15 |
16 | ### Developer Experience 💕😎✨
17 | - VSCode concurrent separate terminals debugging client, server and tests
18 | - Webpack Hot Reloading with Cache
19 | - Git Pre-Push Hook that run tests and blocks bad commits
20 | - Repositoryless shared code packages (bundled by webpack)
21 | - Deploy Ready for Google Cloud (Bucket, Branch Container > Artifact Registry > Run, Compute, Function, GKE)
22 | - [Automated Backend](https://github.com/ruyd/automated-express-backend)
23 | ### Made with
24 |
25 | - TypeScript
26 | - React, Redux and React Query
27 | - Material UI
28 | - Swagger
29 | - Sequelize
30 | - Postgres
31 | - Webpack
32 | - Jest and Docker
33 | - Auth0
34 | - socket.io
35 |
36 | ### Quick Start
37 |
38 | - `git clone https://github.com/ruyd/fullstack-monorepo desiredName`
39 | - `yarn dev` or open in vscode and run debug
40 | - Enter email in start page and go to settings admin
41 |
42 | ### Best with
43 | - [Docker](https://www.docker.com/)
44 |
45 | ### About Drawspace App
46 |
47 | A drawing web and mobile application that allows users to sketch on an empty piece of “paper” and upload it to a public drawings market
48 |
49 | [](https://drawspace.app)
50 |
51 | [](https://opensource.org/licenses/ISC)
52 |
53 |
--------------------------------------------------------------------------------
/workspaces/lib/src/types/order.ts:
--------------------------------------------------------------------------------
1 | import { CartType, Entity, Price, Product, Subscription } from '.'
2 | import { User } from './user'
3 | import { Drawing } from './drawing'
4 |
5 | export const OrderStatus = {
6 | Pending: 'pending',
7 | Paid: 'paid',
8 | Shipped: 'shipped',
9 | Delivered: 'delivered',
10 | Cancelled: 'cancelled'
11 | } as const
12 |
13 | export type OrderStatus = typeof OrderStatus[keyof typeof OrderStatus]
14 |
15 | export interface OrderItem extends Entity {
16 | orderItemId?: string
17 | orderId?: string
18 | drawingId?: string
19 | productId?: string
20 | priceId?: string
21 | paid?: number
22 | quantity?: number
23 | tokens?: number
24 | drawing?: Drawing
25 | type?: CartType
26 | product?: Partial
27 | }
28 |
29 | export interface Order extends Entity {
30 | orderId?: string
31 | userId?: string
32 | billingAddressId?: string
33 | shippingAddressId?: string
34 |
35 | paymentType?: PaymentType
36 | paymentIntentId?: string
37 | payment?: {
38 | [key: string]: unknown
39 | }
40 | total?: number
41 | status?: OrderStatus
42 | OrderItems?: OrderItem[]
43 | user?: User
44 | subscription?: Subscription
45 | }
46 |
47 | export const PaymentTypes = {
48 | Subscription: 'subscription',
49 | OneTime: 'onetime'
50 | } as const
51 |
52 | export type PaymentType = typeof PaymentTypes[keyof typeof PaymentTypes]
53 |
54 | export interface Payment extends Entity {
55 | paymentId: string
56 | userId: string
57 | orderId: string
58 | amount: number
59 | currency: string
60 | status?: string
61 | }
62 |
63 | export const CaptureStatus = {
64 | Successful: 'completed',
65 | Pending: 'pending',
66 | Failed: 'failed',
67 | Created: 'created'
68 | } as const
69 |
70 | export const StripeToCaptureStatusMap = {
71 | canceled: CaptureStatus.Failed,
72 | succeeded: CaptureStatus.Successful,
73 | processing: CaptureStatus.Pending,
74 | requires_action: CaptureStatus.Pending,
75 | requires_capture: CaptureStatus.Pending,
76 | requires_confirmation: CaptureStatus.Pending,
77 | requires_payment_method: CaptureStatus.Pending,
78 | unknown: CaptureStatus.Failed
79 | } as const
80 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/MenuItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import List from '@mui/material/List'
3 | import ListItemButton from '@mui/material/ListItemButton'
4 | import ListItemIcon from '@mui/material/ListItemIcon'
5 | import ListItemText from '@mui/material/ListItemText'
6 | import Collapse from '@mui/material/Collapse'
7 | import ExpandLess from '@mui/icons-material/ExpandLess'
8 | import ExpandMore from '@mui/icons-material/ExpandMore'
9 | import { Link } from 'react-router-dom'
10 | import { config } from '../../shared/config'
11 |
12 | export interface MenuModel {
13 | text: string
14 | icon?: React.ReactNode
15 | path?: string
16 | children?: MenuModel[]
17 | selected?: boolean
18 | }
19 |
20 | export default function MenuItem({
21 | item,
22 | onChange,
23 | }: {
24 | item: MenuModel
25 | onChange?: (item: MenuModel) => void
26 | active?: boolean
27 | }) {
28 | const [open, setOpen] = React.useState(true)
29 | const { text, icon, children, path, selected } = item
30 | const handleClick = () => {
31 | setOpen(!open)
32 | if (onChange) {
33 | onChange(item)
34 | }
35 | }
36 | const linkProps = () =>
37 | path
38 | ? {
39 | component: Link,
40 | to: config.admin.path + path,
41 | }
42 | : {}
43 |
44 | return (
45 | <>
46 |
47 | {icon}
48 |
49 | {children ? open ? : : null}
50 |
51 |
52 |
53 | {children?.map(child => (
54 |
59 | {child.icon ? {child.icon} : null}
60 |
61 |
62 | ))}
63 |
64 |
65 | >
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/workspaces/client/src/shared/testing.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react'
2 | import { QueryClient, QueryClientProvider } from 'react-query'
3 | import { Provider } from 'react-redux'
4 | import { BrowserRouter } from 'react-router-dom'
5 | import { configureStore } from '@reduxjs/toolkit'
6 | import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore'
7 |
8 | import { type RootState, getStore, useAppDispatch, useAppSelector } from 'src/shared/store'
9 | const { reducers } = jest.requireActual('src/shared/store')
10 | jest.mock('src/shared/store', () => ({
11 | getStore: jest.fn(),
12 | useAppSelector: jest.fn(),
13 | useAppDispatch: jest.fn()
14 | }))
15 |
16 | export const mockStore = (state?: Partial) => {
17 | const preloadedState = {
18 | app: {
19 | locale: 'en',
20 | dialog: 'onboard',
21 | settings: {
22 | system: {
23 | authProvider: 'firebase'
24 | }
25 | }
26 | },
27 | ...state
28 | } as RootState
29 |
30 | const testStore = configureStore({
31 | reducer: reducers,
32 | preloadedState
33 | }) as ToolkitStore
34 |
35 | jest.mocked(getStore).mockReturnValue(testStore)
36 | jest.mocked(useAppDispatch).mockImplementation(() => testStore.dispatch)
37 | jest.mocked(useAppSelector).mockImplementation(selector => selector(testStore.getState()))
38 |
39 | return testStore
40 | }
41 |
42 | export const renderWithContext = async (
43 | element: React.ReactElement,
44 | state?: Partial
45 | ) => {
46 | const preloadedState = {
47 | app: {
48 | locale: 'en',
49 | dialog: 'onboard',
50 | settings: {
51 | system: {
52 | authProvider: 'firebase'
53 | }
54 | }
55 | },
56 | ...state
57 | } as RootState
58 | const testStore = mockStore(preloadedState)
59 | const testQueryClient = new QueryClient({})
60 | return {
61 | ...render(
62 |
63 |
64 | {element}
65 |
66 |
67 | ),
68 | testStore,
69 | testQueryClient
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/workspaces/server/src/routes/shop/paypal.ts:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 |
3 | const { CLIENT_ID, APP_SECRET } = process['env']
4 | const base = 'https://api-m.sandbox.paypal.com'
5 |
6 | export async function capturePaymentHandler(req: express.Request, res: express.Response) {
7 | const { orderID } = req.params
8 | const captureData = await capturePayment(orderID)
9 | // TODO: store payment information such as the transaction ID
10 | res.json(captureData)
11 | }
12 |
13 | export async function createOrderHandler(req: express.Request, res: express.Response) {
14 | const orderData = await createOrder()
15 | res.json(orderData)
16 | }
17 |
18 | // use the orders api to create an order
19 | async function createOrder() {
20 | const accessToken = await generateAccessToken()
21 | const url = `${base}/v2/checkout/orders`
22 | const response = await fetch(url, {
23 | method: 'post',
24 | headers: {
25 | 'Content-Type': 'application/json',
26 | Authorization: `Bearer ${accessToken}`,
27 | },
28 | body: JSON.stringify({
29 | intent: 'CAPTURE',
30 | purchase_units: [
31 | {
32 | amount: {
33 | currency_code: 'USD',
34 | value: '100.00',
35 | },
36 | },
37 | ],
38 | }),
39 | })
40 | const data = await response.json()
41 | return data
42 | }
43 |
44 | async function capturePayment(orderId: string) {
45 | const accessToken = await generateAccessToken()
46 | const url = `${base}/v2/checkout/orders/${orderId}/capture`
47 | const response = await fetch(url, {
48 | method: 'post',
49 | headers: {
50 | 'Content-Type': 'application/json',
51 | Authorization: `Bearer ${accessToken}`,
52 | },
53 | })
54 | const data = await response.json()
55 | return data
56 | }
57 |
58 | async function generateAccessToken() {
59 | const auth = Buffer.from(CLIENT_ID + ':' + APP_SECRET).toString('base64')
60 | const response = await fetch(`${base}/v1/oauth2/token`, {
61 | method: 'post',
62 | body: 'grant_type=client_credentials',
63 | headers: {
64 | Authorization: `Basic ${auth}`,
65 | },
66 | })
67 | const data = await response.json()
68 | return data.access_token
69 | }
70 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/Card/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import useTheme from '@mui/material/styles/useTheme'
4 | import CardContent from '@mui/material/CardContent'
5 | import Divider from '@mui/material/Divider'
6 | import Typography from '@mui/material/Typography'
7 | import CardHeader from '@mui/material/CardHeader'
8 | import Card from '@mui/material/Card'
9 |
10 | // constant
11 | const headerSX = {
12 | '& .MuiCardHeader-action': { mr: 0 }
13 | }
14 |
15 | export interface CardProps {
16 | border?: boolean
17 | boxShadow?: boolean
18 | children?: React.ReactNode
19 | content?: boolean
20 | contentClass?: string
21 | contentSX?: object
22 | darkTitle?: boolean
23 | secondary?: React.ReactNode
24 | shadow?: string
25 | sx?: object
26 | title?: React.ReactNode
27 | }
28 |
29 | // ==============================|| CUSTOM MAIN CARD ||============================== //
30 |
31 | export function MainCard({
32 | border = true,
33 | boxShadow,
34 | children,
35 | content = true,
36 | contentClass = '',
37 | contentSX = {},
38 | darkTitle,
39 | secondary,
40 | shadow,
41 | sx = {},
42 | title,
43 | ...others
44 | }: CardProps) {
45 | const theme = useTheme()
46 | return (
47 |
58 | <>
59 | {/* card header and action */}
60 | {!darkTitle && title && }
61 | {darkTitle && title && (
62 | {title}}
65 | action={secondary}
66 | />
67 | )}
68 |
69 | {/* content & header divider */}
70 | {title && }
71 |
72 | {/* card content */}
73 | {content && (
74 |
75 | {children}
76 |
77 | )}
78 | {!content && children}
79 | >
80 |
81 | )
82 | }
83 |
84 | export default MainCard
85 |
--------------------------------------------------------------------------------
/workspaces/server/src/app.ts:
--------------------------------------------------------------------------------
1 | import { config } from './shared/config'
2 | import express from 'express'
3 | import bodyParser from 'body-parser'
4 | import swaggerUi from 'swagger-ui-express'
5 | import { prepareSwagger } from './shared/model-api/swagger'
6 | import { registerModelApiRoutes } from './shared/model-api/routes'
7 | import { errorHandler } from './shared/errorHandler'
8 | import cors from 'cors'
9 | import api from './routes'
10 | import { activateAxiosTrace, endpointTracingMiddleware, printRouteSummary } from './shared/trace'
11 | import { Connection } from './shared/db'
12 | import { checkDatabase } from './shared/db/check'
13 | import { modelAuthMiddleware } from './shared/auth'
14 | import { homepage } from './shared/server'
15 |
16 | export interface BackendApp extends express.Express {
17 | onStartupCompletePromise: Promise
18 | }
19 |
20 | export interface BackendOptions {
21 | checks?: boolean
22 | trace?: boolean
23 | }
24 |
25 | export function createBackendApp({ checks, trace }: BackendOptions = { checks: true }): BackendApp {
26 | const app = express() as BackendApp
27 |
28 | if (trace !== undefined) {
29 | config.trace = trace
30 | config.db.trace = trace
31 | }
32 |
33 | if (!config.production && config.trace) {
34 | activateAxiosTrace()
35 | }
36 |
37 | // Startup
38 | Connection.init()
39 | const promises = [checks ? checkDatabase() : Promise.resolve(true)]
40 |
41 | // Add Middlewares
42 | app.use(cors())
43 | app.use(express.json({ limit: config.jsonLimit }))
44 | app.use(
45 | bodyParser.urlencoded({
46 | extended: true
47 | })
48 | )
49 | app.use(endpointTracingMiddleware)
50 | app.use(modelAuthMiddleware)
51 |
52 | // Add Routes
53 | registerModelApiRoutes(Connection.entities, api)
54 | app.use(api)
55 |
56 | const swaggerDoc = prepareSwagger(app, Connection.entities)
57 | app.use(
58 | config.swaggerSetup.basePath,
59 | swaggerUi.serve,
60 | swaggerUi.setup(swaggerDoc, {
61 | customSiteTitle: config.swaggerSetup.info?.title,
62 | swaggerOptions: {
63 | persistAuthorization: true
64 | }
65 | })
66 | )
67 |
68 | app.onStartupCompletePromise = Promise.all(promises)
69 |
70 | printRouteSummary(app)
71 |
72 | app.get('/', homepage)
73 |
74 | app.use(errorHandler)
75 |
76 | return app
77 | }
78 |
79 | export default createBackendApp
80 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/app/SocketListener.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import React from 'react'
3 | import { io, Socket } from 'socket.io-client'
4 |
5 | import { useAppDispatch, useAppSelector } from '../../shared/store'
6 | import { AppNotification, AppState, patch } from '.'
7 | import { config } from '../../shared/config'
8 |
9 | import loadConfig, { setConfig } from '../../shared/loadConfig'
10 | import { ClientConfig } from '@lib'
11 |
12 | export default function SocketListener() {
13 | const dispatch = useAppDispatch()
14 | const socketRef = React.useRef()
15 | const token = useAppSelector(state => state.app.token)
16 | const { activeMenuItem } = useAppSelector(state => state.admin)
17 | const { notifications } = useAppSelector(state => state.app)
18 |
19 | React.useEffect(() => {
20 | if (!token) {
21 | if (socketRef.current) {
22 | socketRef.current.disconnect()
23 | socketRef.current = undefined
24 | }
25 | return
26 | }
27 |
28 | if (socketRef.current) {
29 | return
30 | }
31 |
32 | const socket = io(config.backendUrl, {
33 | reconnectionAttempts: 3,
34 | reconnectionDelay: 1000,
35 | auth: {
36 | token,
37 | },
38 | })
39 | socketRef.current = socket
40 | socket.on('connect', () => {
41 | console.log('connected')
42 | })
43 |
44 | socket.on('disconnect', () => {
45 | console.log('disconnected')
46 | })
47 |
48 | socket.onAny((eventName, type, payload) => {
49 | // called for each packet received
50 | console.log('backend', eventName, type, payload)
51 | })
52 |
53 | socket.on('ban', () => {
54 | dispatch(patch({ token: undefined }))
55 | })
56 |
57 | socket.on('setting', () => {
58 | loadConfig()
59 | })
60 |
61 | socket.on('state', (state: AppState) => {
62 | dispatch(patch({ ...state }))
63 | })
64 |
65 | socket.on('config', (config: ClientConfig) => {
66 | setConfig(config)
67 | })
68 |
69 | socket.on('notification', (notification: AppNotification) => {
70 | dispatch(patch({ notifications: [...notifications, notification] }))
71 | })
72 | }, [dispatch, notifications, token])
73 |
74 | React.useEffect(() => {
75 | if (socketRef.current) {
76 | socketRef.current.emit('menu', activeMenuItem)
77 | }
78 | }, [activeMenuItem])
79 |
80 | return null
81 | }
82 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/profile/Callback.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Alert from '@mui/material/Alert'
3 | import authProvider from 'auth0-js'
4 | import { getAuth0Settings, getNonce } from '../../shared/auth'
5 | import loginImage from './images/login.svg'
6 | import { useAppSelector } from '../../shared/store'
7 | import Paper from '@mui/material/Paper'
8 | import Grid from '@mui/material/Grid'
9 | import AlertTitle from '@mui/material/AlertTitle'
10 | import Typography from '@mui/material/Typography'
11 | import Box from '@mui/material/Box'
12 | import LinearProgress from '@mui/material/LinearProgress'
13 |
14 | export default function Callback(): JSX.Element {
15 | const clientId = useAppSelector(state => state.app.settings?.auth0?.clientId)
16 | const access_token = new URLSearchParams(window.location.hash).get('#access_token')
17 | const id_token = new URLSearchParams(window.location.href).get('id_token')
18 | const state = new URLSearchParams(window.location.href).get('state') as string
19 | const error = new URLSearchParams(window.location.hash).get('#error')
20 | const errorDescription = new URLSearchParams(window.location.href).get('error_description')
21 | React.useEffect(() => {
22 | const parseCallback = async () => {
23 | const { state, nonce } = getNonce()
24 | const options = { ...getAuth0Settings(), state, nonce, clientId }
25 | const webAuth = new authProvider.WebAuth(options)
26 | webAuth.popup.callback({
27 | hash: window.location.hash,
28 | state,
29 | nonce
30 | })
31 | }
32 | if (access_token && id_token) {
33 | parseCallback()
34 | }
35 | parseCallback()
36 | }, [access_token, id_token, state, clientId])
37 |
38 | return (
39 |
40 |
41 |
42 | {error && (
43 |
44 | {errorDescription}
45 |
46 | )}
47 |
48 | Signing in...
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/workspaces/server/src/shared/socket/index.ts:
--------------------------------------------------------------------------------
1 | import { Server } from 'http'
2 | import { Server as ServerHttps } from 'https'
3 | import { Server as SocketService, Socket } from 'socket.io'
4 | import { decodeToken } from '../auth'
5 | import logger from '../logger'
6 | import { createOrUpdate } from '../model-api/controller'
7 | import { UserActiveModel } from '../types'
8 | import handlers from './handlers'
9 | import { config } from '../config'
10 | import { getClientSettings } from '../settings'
11 |
12 | export type SocketHandler = (io: SocketService, socket: Socket) => void
13 |
14 | export let io: SocketService
15 |
16 | function onDisconnect(socket: Socket) {
17 | logger.info('- socket disconnected: ' + socket.id)
18 | try {
19 | UserActiveModel.destroy({
20 | where: {
21 | socketId: socket.id
22 | }
23 | })
24 | } catch (error) {
25 | logger.error(error)
26 | }
27 | }
28 |
29 | function onConnect(socket: Socket) {
30 | logger.info(`⚡️ [socket]: New connection: ${socket.id}`)
31 | try {
32 | const decoded = decodeToken(socket.handshake.auth.token)
33 | logger.info('decoded' + JSON.stringify(decoded))
34 | createOrUpdate(UserActiveModel, {
35 | socketId: socket.id,
36 | userId: decoded?.uid,
37 | ip: socket.handshake.address,
38 | userAgent: socket.handshake.headers['user-agent']
39 | })
40 | } catch (error) {
41 | logger.error(error)
42 | }
43 | }
44 |
45 | export function registerSocket(server: Server | ServerHttps): void {
46 | io = new SocketService(server, {
47 | cors: config.cors
48 | })
49 | const onConnection = (socket: Socket) => {
50 | handlers.forEach((handler: SocketHandler) => handler(io, socket))
51 |
52 | socket.send('Helo', {
53 | notifications: ['Hi!']
54 | })
55 |
56 | onConnect(socket)
57 |
58 | socket.on('disconnect', () => onDisconnect(socket))
59 | }
60 | io.on('connection', onConnection)
61 | }
62 |
63 | export async function broadcastChange(eventName: string, data: unknown): Promise {
64 | // const sockets = await io.except(userId).fetchSockets()
65 | // io.except(userId).emit('config', { data })
66 | io.emit(eventName, { data })
67 | }
68 |
69 | export async function notifyChange(eventName: string): Promise {
70 | io.emit(eventName)
71 | }
72 |
73 | export async function sendConfig(): Promise {
74 | const payload = await getClientSettings(false, true)
75 | io.emit('config', { ...payload })
76 | }
77 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "author": {
4 | "name": "ruyd"
5 | },
6 | "license": "ISC",
7 | "private": true,
8 | "workspaces": [
9 | "workspaces/*"
10 | ],
11 | "outDir": "dist",
12 | "scripts": {
13 | "start": "HEROKU=true NODE_ENV=production yarn workspace server start",
14 | "build": "HEROKU=true yarn workspace server build",
15 | "build:tests": "yarn tsc -b workspaces/*/tests -v",
16 | "build:check": "yarn tsc -b workspaces/*/tsconfig.json -v",
17 | "prod": "NODE_ENV=production concurrently \"yarn workspace server start\" \"yarn workspace client start\"",
18 | "dev": "yarn install && docker compose up --wait && concurrently \"yarn workspace server dev\" \"yarn workspace client dev\" \"yarn workspace lib dev\"",
19 | "client": "yarn workspace client dev",
20 | "server": "yarn workspace server dev",
21 | "docker": "docker compose up --wait",
22 | "lint": "eslint .",
23 | "test": "yarn build && yarn docker && yarn workspace server test && yarn workspace client test",
24 | "prettify": "yarn prettier . --write",
25 | "clean": "rm -rf node_modules workspaces/*/node_modules package-lock.json yarn.lock workspaces/*/build workspaces/*/dist",
26 | "precommit": "yarn build:check",
27 | "prepare": "husky install"
28 | },
29 | "devDependencies": {
30 | "@jest/types": "^29.3.1",
31 | "@types/compression": "^1.7.2",
32 | "@types/express": "^4.17.14",
33 | "@types/html-minifier-terser": "^6.1.0",
34 | "@types/node": "^18.11.9",
35 | "@types/offscreencanvas": "^2019.7.0",
36 | "@types/react": "^18.0.15",
37 | "@types/react-dom": "^18.0.6",
38 | "@types/uuid": "^8.3.4",
39 | "@typescript-eslint/eslint-plugin": "^5.43.0",
40 | "@typescript-eslint/parser": "^5.49.0",
41 | "concurrently": "^8.1.0",
42 | "eslint": "^8.42.0",
43 | "eslint-config-prettier": "^8.8.0",
44 | "eslint-config-standard-with-typescript": "^35.0.0",
45 | "eslint-import-resolver-typescript": "^3.5.5",
46 | "eslint-plugin-import": "^2.27.5",
47 | "eslint-plugin-n": "^16.0.0",
48 | "eslint-plugin-promise": "^6.1.1",
49 | "eslint-plugin-react": "^7.32.2",
50 | "gh-pages": "^5.0.0",
51 | "husky": "^8.0.3",
52 | "jest": "^29.5.0",
53 | "pg": "^8.11.0",
54 | "prettier": "^2.8.8",
55 | "sequelize": "^6.32.0",
56 | "ts-jest": "^29.1.0",
57 | "ts-node": "^10.9.1",
58 | "typescript": "^5.1.3",
59 | "umzug": "^3.2.1"
60 | },
61 | "packageManager": "yarn@3.3.0"
62 | }
63 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/home/images/nodejs.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/admin/Dashboard/chart-data/total-growth-bar-chart.ts:
--------------------------------------------------------------------------------
1 | // ===========================|| DASHBOARD - TOTAL GROWTH BAR CHART ||=========================== //
2 |
3 | const chartData = {
4 | height: 480,
5 | type: 'bar',
6 | options: {
7 | chart: {
8 | id: 'bar-chart',
9 | stacked: true,
10 | toolbar: {
11 | show: true
12 | },
13 | zoom: {
14 | enabled: true
15 | }
16 | },
17 | responsive: [
18 | {
19 | breakpoint: 480,
20 | options: {
21 | legend: {
22 | position: 'bottom',
23 | offsetX: -10,
24 | offsetY: 0
25 | }
26 | }
27 | }
28 | ],
29 | plotOptions: {
30 | bar: {
31 | horizontal: false,
32 | columnWidth: '50%'
33 | }
34 | },
35 | xaxis: {
36 | type: 'category',
37 | categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
38 | },
39 | legend: {
40 | show: true,
41 | fontSize: '14px',
42 | fontFamily: `'Roboto', sans-serif`,
43 | position: 'bottom',
44 | offsetX: 20,
45 | labels: {
46 | useSeriesColors: false
47 | },
48 | markers: {
49 | width: 16,
50 | height: 16,
51 | radius: 5
52 | },
53 | itemMargin: {
54 | horizontal: 15,
55 | vertical: 8
56 | }
57 | },
58 | fill: {
59 | type: 'solid'
60 | },
61 | dataLabels: {
62 | enabled: false
63 | },
64 | grid: {
65 | show: true
66 | }
67 | },
68 | series: [
69 | {
70 | name: 'Investment',
71 | data: [35, 125, 35, 35, 35, 80, 35, 20, 35, 45, 15, 75]
72 | },
73 | {
74 | name: 'Loss',
75 | data: [35, 15, 15, 35, 65, 40, 80, 25, 15, 85, 25, 75]
76 | },
77 | {
78 | name: 'Profit',
79 | data: [35, 145, 35, 35, 20, 105, 100, 10, 65, 45, 30, 10]
80 | },
81 | {
82 | name: 'Maintenance',
83 | data: [0, 0, 75, 0, 0, 115, 0, 0, 0, 0, 150, 0]
84 | }
85 | ]
86 | };
87 | export default chartData;
88 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/shop/Payment.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Typography from '@mui/material/Typography'
3 | import StripeCheckout from './StripeCheckout'
4 | import { useAppSelector } from 'src/shared/store'
5 | import OrderAddress from './OrderAddress'
6 | import OrderItems from './OrderItems'
7 | import FakeCheckout from './FakeCheckout'
8 | import Box from '@mui/material/Box'
9 | import Stack from '@mui/material/Stack'
10 | import List from '@mui/material/List'
11 | import ListItem from '@mui/material/ListItem'
12 | import ListItemButton from '@mui/material/ListItemButton'
13 | import ListItemText from '@mui/material/ListItemText'
14 |
15 | export default function PaymentStep() {
16 | const settings = useAppSelector(state => state.app.settings?.system?.paymentMethods)
17 | const [option, setOption] = React.useState<
18 | | {
19 | name: string
20 | component: JSX.Element
21 | label: string
22 | }
23 | | undefined
24 | >()
25 |
26 | const options = [
27 | {
28 | name: 'stripe',
29 | component: ,
30 | label: 'Card',
31 | enabled: settings?.stripe?.enabled
32 | },
33 | {
34 | name: 'paypal',
35 | component: <>>,
36 | label: 'PayPal',
37 | enabled: settings?.paypal?.enabled
38 | },
39 | {
40 | name: 'fake',
41 | component: ,
42 | label: 'Just Fake Payment',
43 | enabled: process.env.NODE_ENV === 'development'
44 | }
45 | ]
46 |
47 | return (
48 |
56 |
57 |
58 |
59 |
60 |
61 | {!option && (
62 |
63 | How do you want to pay?
64 |
65 | {options
66 | .filter(a => a.enabled)
67 | .map(item => (
68 |
69 | setOption(item)} selected={option === item.name}>
70 |
71 |
72 |
73 | ))}
74 |
75 |
76 | )}
77 | {option && option.component}
78 |
79 |
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/workspaces/client/src/features/ui/extended/AnimateButton.tsx:
--------------------------------------------------------------------------------
1 | import { motion, useCycle } from 'framer-motion'
2 |
3 | export interface AnimateButtonProps {
4 | children?: React.ReactNode
5 | offset?: number
6 | type?: 'slide' | 'scale' | 'rotate'
7 | direction?: 'up' | 'down' | 'left' | 'right'
8 | scale?: number | { hover?: number; tap?: number }
9 | }
10 | export function AnimateButton(
11 | { children, type, direction, offset, scale }: AnimateButtonProps = {
12 | type: 'scale',
13 | direction: 'right',
14 | offset: 10,
15 | scale: { hover: 1, tap: 0.9 },
16 | },
17 | ref?: React.MutableRefObject,
18 | ) {
19 | let offset1
20 | let offset2
21 | switch (direction) {
22 | case 'up':
23 | case 'left':
24 | offset1 = offset
25 | offset2 = 0
26 | break
27 | case 'right':
28 | case 'down':
29 | default:
30 | offset1 = 0
31 | offset2 = offset
32 | break
33 | }
34 |
35 | const [x, cycleX] = useCycle(offset1, offset2)
36 | const [y, cycleY] = useCycle(offset1, offset2)
37 |
38 | switch (type) {
39 | case 'rotate':
40 | return (
41 |
51 | {children}
52 |
53 | )
54 | case 'slide':
55 | if (direction === 'up' || direction === 'down') {
56 | return (
57 | cycleY()}
61 | onHoverStart={() => cycleY()}
62 | >
63 | {children}
64 |
65 | )
66 | }
67 | return (
68 | cycleX()}
72 | onHoverStart={() => cycleX()}
73 | >
74 | {children}
75 |
76 | )
77 |
78 | case 'scale':
79 | default:
80 | if (typeof scale === 'number') {
81 | scale = {
82 | hover: scale,
83 | tap: scale,
84 | }
85 | }
86 | return (
87 |
88 | {children}
89 |
90 | )
91 | }
92 | }
93 |
94 | export default AnimateButton
95 |
--------------------------------------------------------------------------------