├── .editorconfig
├── .env
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode
└── extensions.json
├── README.md
├── apps
├── api-e2e
│ ├── .eslintrc.json
│ ├── jest.config.ts
│ ├── project.json
│ ├── src
│ │ ├── api
│ │ │ └── api.spec.ts
│ │ └── support
│ │ │ ├── global-setup.ts
│ │ │ ├── global-teardown.ts
│ │ │ └── test-setup.ts
│ ├── tsconfig.json
│ └── tsconfig.spec.json
├── api
│ ├── .eslintrc.json
│ ├── jest.config.ts
│ ├── project.json
│ ├── src
│ │ ├── app
│ │ │ ├── .gitkeep
│ │ │ └── app.module.ts
│ │ ├── assets
│ │ │ └── .gitkeep
│ │ ├── environments
│ │ │ ├── environment.prod.ts
│ │ │ └── environment.ts
│ │ └── main.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.spec.json
│ └── webpack.config.js
├── web-e2e
│ ├── .eslintrc.json
│ ├── playwright.config.ts
│ ├── project.json
│ ├── src
│ │ └── example.spec.ts
│ └── tsconfig.json
└── web
│ ├── .babelrc
│ ├── .eslintrc.json
│ ├── jest.config.ts
│ ├── project.json
│ ├── src
│ ├── app
│ │ ├── app.context.tsx
│ │ ├── app.tsx
│ │ ├── data-access
│ │ │ ├── db.ts
│ │ │ ├── donasi.model.ts
│ │ │ ├── donasi.schema.ts
│ │ │ ├── local.ts
│ │ │ └── server.ts
│ │ ├── footer
│ │ │ ├── footer.tsx
│ │ │ └── link.tsx
│ │ ├── header
│ │ │ └── header.tsx
│ │ ├── icons
│ │ │ ├── add.svg
│ │ │ ├── back.svg
│ │ │ ├── home.svg
│ │ │ ├── more.svg
│ │ │ ├── search.svg
│ │ │ └── sync.svg
│ │ └── main
│ │ │ ├── add
│ │ │ └── add.tsx
│ │ │ ├── list
│ │ │ ├── list.tsx
│ │ │ ├── local-data.tsx
│ │ │ ├── server-data.tsx
│ │ │ └── summary.tsx
│ │ │ ├── main.tsx
│ │ │ ├── search
│ │ │ ├── search.spec.tsx
│ │ │ └── search.tsx
│ │ │ └── sync
│ │ │ └── sync.tsx
│ ├── assets
│ │ ├── .gitkeep
│ │ └── icons
│ │ │ ├── icon-128x128.png
│ │ │ ├── icon-144x144.png
│ │ │ ├── icon-152x152.png
│ │ │ ├── icon-192x192.png
│ │ │ ├── icon-384x384.png
│ │ │ ├── icon-512x512.png
│ │ │ ├── icon-72x72.png
│ │ │ └── icon-96x96.png
│ ├── favicon.ico
│ ├── index.html
│ ├── main.tsx
│ ├── manifest.webmanifest
│ ├── serviceworker.js
│ └── styles.css
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.spec.json
│ └── webpack.config.js
├── docker-compose.yml
├── jest.config.ts
├── jest.preset.js
├── libs
├── gql
│ ├── .eslintrc.json
│ ├── README.md
│ ├── jest.config.ts
│ ├── project.json
│ ├── src
│ │ ├── gql.module.ts
│ │ ├── index.ts
│ │ ├── models
│ │ │ ├── batch-payload.ts
│ │ │ ├── donasi-create-input.ts
│ │ │ ├── donasi-list-input.ts
│ │ │ ├── donasi-pagelist-input.ts
│ │ │ └── donasi.ts
│ │ └── resolvers
│ │ │ └── donasi.resolver.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ └── tsconfig.spec.json
└── services
│ ├── .eslintrc.json
│ ├── README.md
│ ├── jest.config.ts
│ ├── project.json
│ ├── src
│ ├── donasi
│ │ └── donasi.service.ts
│ ├── index.ts
│ ├── prisma
│ │ └── prisma.service.ts
│ └── services.module.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ └── tsconfig.spec.json
├── nx.json
├── package-lock.json
├── package.json
├── prisma
└── schema.prisma
├── screenshot
├── 1.png
└── 2.png
└── tsconfig.base.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # Environment variables declared in this file are automatically made available to Prisma.
2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables
3 |
4 | # Prisma supports the native connection string format for PostgreSQL, MySQL and SQLite.
5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings
6 |
7 | ADMINER_PORT=7000
8 |
9 | POSTGRES_USER=postgres
10 | POSTGRES_PASSWORD=kosong
11 | POSTGRES_PORT=54321
12 | POSTGRES_DB=offline
13 |
14 | DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public"
15 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": ["**/*"],
4 | "plugins": ["@nx"],
5 | "overrides": [
6 | {
7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
8 | "rules": {
9 | "@nx/enforce-module-boundaries": [
10 | "error",
11 | {
12 | "enforceBuildableLibDependency": true,
13 | "allow": [],
14 | "depConstraints": [
15 | {
16 | "sourceTag": "*",
17 | "onlyDependOnLibsWithTags": ["*"]
18 | }
19 | ]
20 | }
21 | ]
22 | }
23 | },
24 | {
25 | "files": ["*.ts", "*.tsx"],
26 | "extends": ["plugin:@nx/typescript"],
27 | "rules": {}
28 | },
29 | {
30 | "files": ["*.js", "*.jsx"],
31 | "extends": ["plugin:@nx/javascript"],
32 | "rules": {}
33 | },
34 | {
35 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"],
36 | "env": {
37 | "jest": true
38 | },
39 | "rules": {}
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | dist
5 | tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | yarn-error.log
34 | testem.log
35 | /typings
36 |
37 | # System Files
38 | .DS_Store
39 | Thumbs.db
40 |
41 | .nx/cache
42 |
43 | /prisma/migrations
44 | /prisma/schema.gql
45 | migrations.json
46 | debug.log
47 | todo.txt
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 | /dist
3 | /coverage
4 | /.nx/cache
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "nrwl.angular-console",
4 | "esbenp.prettier-vscode",
5 | "firsttris.vscode-jest-runner",
6 | "ms-playwright.playwright"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Offline
2 |
3 | Sample offline first using Nx monorepo, React, Mantine, RxDb, NestJs, Prisma, GraphQl, PostgreSql
4 |
5 | ## Install
6 |
7 | clone this repo `git clone https://github.com/madipta/offline-first.git`
8 |
9 | goto folder
10 | `cd offline-first`
11 |
12 | install node_modules packages
13 | `npm install` or `yarn`
14 |
15 | run postgresql using docker
16 | `docker-compose up`
17 |
18 | make sure database service is running before generate database
19 |
20 | then generate database
21 | `npm run migrate:dev` or `yarn run migrate:dev`
22 |
23 | generate prisma client
24 | `npm run prisma:generate` or `yarn run prisma:generate`
25 |
26 | ## How to Run
27 |
28 | run nest api server
29 | `npx nx serve api` or `yarn nx serve api`
30 |
31 | run React web server
32 | `npx nx serve` or `yarn nx serve`
33 |
34 | open browser http://localhost:4200
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/apps/api-e2e/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/apps/api-e2e/jest.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export default {
3 | displayName: 'api-e2e',
4 | preset: '../../jest.preset.js',
5 | globalSetup: '/src/support/global-setup.ts',
6 | globalTeardown: '/src/support/global-teardown.ts',
7 | setupFiles: ['/src/support/test-setup.ts'],
8 | testEnvironment: 'node',
9 | transform: {
10 | '^.+\\.[tj]s$': [
11 | 'ts-jest',
12 | {
13 | tsconfig: '/tsconfig.spec.json',
14 | },
15 | ],
16 | },
17 | moduleFileExtensions: ['ts', 'js', 'html'],
18 | coverageDirectory: '../../coverage/api-e2e',
19 | };
20 |
--------------------------------------------------------------------------------
/apps/api-e2e/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api-e2e",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "implicitDependencies": ["api"],
5 | "projectType": "application",
6 | "targets": {
7 | "e2e": {
8 | "executor": "@nx/jest:jest",
9 | "outputs": ["{workspaceRoot}/coverage/{e2eProjectRoot}"],
10 | "options": {
11 | "jestConfig": "apps/api-e2e/jest.config.ts",
12 | "passWithNoTests": true
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/api-e2e/src/api/api.spec.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | describe('GET /api', () => {
4 | it('should return a message', async () => {
5 | const res = await axios.get(`/api`);
6 |
7 | expect(res.status).toBe(200);
8 | expect(res.data).toEqual({ message: 'Hello API' });
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/apps/api-e2e/src/support/global-setup.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var __TEARDOWN_MESSAGE__: string;
3 |
4 | module.exports = async function () {
5 | // Start services that that the app needs to run (e.g. database, docker-compose, etc.).
6 | console.log('\nSetting up...\n');
7 |
8 | // Hint: Use `globalThis` to pass variables to global teardown.
9 | globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n';
10 | };
11 |
--------------------------------------------------------------------------------
/apps/api-e2e/src/support/global-teardown.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | module.exports = async function () {
4 | // Put clean up logic here (e.g. stopping services, docker-compose, etc.).
5 | // Hint: `globalThis` is shared between setup and teardown.
6 | console.log(globalThis.__TEARDOWN_MESSAGE__);
7 | };
8 |
--------------------------------------------------------------------------------
/apps/api-e2e/src/support/test-setup.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import axios from 'axios';
4 |
5 | module.exports = async function () {
6 | // Configure axios for tests to use.
7 | const host = process.env.HOST ?? 'localhost';
8 | const port = process.env.PORT ?? '3000';
9 | axios.defaults.baseURL = `http://${host}:${port}`;
10 | };
11 |
--------------------------------------------------------------------------------
/apps/api-e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.spec.json"
8 | }
9 | ],
10 | "compilerOptions": {
11 | "esModuleInterop": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/apps/api-e2e/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["jest.config.ts", "src/**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/apps/api/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/apps/api/jest.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export default {
3 | displayName: 'api',
4 | preset: '../../jest.preset.js',
5 | testEnvironment: 'node',
6 | transform: {
7 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }],
8 | },
9 | moduleFileExtensions: ['ts', 'js', 'html'],
10 | coverageDirectory: '../../coverage/apps/api',
11 | };
12 |
--------------------------------------------------------------------------------
/apps/api/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "sourceRoot": "apps/api/src",
5 | "projectType": "application",
6 | "targets": {
7 | "serve": {
8 | "executor": "@nx/js:node",
9 | "defaultConfiguration": "development",
10 | "options": {
11 | "buildTarget": "api:build"
12 | },
13 | "configurations": {
14 | "development": {
15 | "buildTarget": "api:build:development"
16 | },
17 | "production": {
18 | "buildTarget": "api:build:production"
19 | }
20 | }
21 | }
22 | },
23 | "tags": []
24 | }
25 |
--------------------------------------------------------------------------------
/apps/api/src/app/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/api/src/app/.gitkeep
--------------------------------------------------------------------------------
/apps/api/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { GqlModule } from '@offline-first/gql';
3 |
4 | @Module({
5 | imports: [GqlModule],
6 | })
7 | export class AppModule {}
8 |
--------------------------------------------------------------------------------
/apps/api/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/api/src/assets/.gitkeep
--------------------------------------------------------------------------------
/apps/api/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | };
4 |
--------------------------------------------------------------------------------
/apps/api/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: false,
3 | };
4 |
--------------------------------------------------------------------------------
/apps/api/src/main.ts:
--------------------------------------------------------------------------------
1 | import { Logger } from '@nestjs/common';
2 | import { NestFactory } from '@nestjs/core';
3 | import { AppModule } from './app/app.module';
4 |
5 | async function bootstrap() {
6 | const app = await NestFactory.create(AppModule);
7 | const globalPrefix = 'graphql';
8 | const port = process.env.PORT || 3333;
9 | app.setGlobalPrefix(globalPrefix);
10 | app.enableCors();
11 | await app.listen(port, () => {
12 | Logger.log('Listening at http://localhost:' + port + '/' + globalPrefix);
13 | });
14 | }
15 |
16 | bootstrap();
17 |
--------------------------------------------------------------------------------
/apps/api/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["node"],
7 | "emitDecoratorMetadata": true,
8 | "target": "es2021"
9 | },
10 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
11 | "include": ["src/**/*.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/apps/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.app.json"
8 | },
9 | {
10 | "path": "./tsconfig.spec.json"
11 | }
12 | ],
13 | "compilerOptions": {
14 | "esModuleInterop": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/api/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": [
9 | "jest.config.ts",
10 | "src/**/*.test.ts",
11 | "src/**/*.spec.ts",
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/apps/api/webpack.config.js:
--------------------------------------------------------------------------------
1 | const { NxWebpackPlugin } = require('@nx/webpack');
2 | const { join } = require('path');
3 |
4 | module.exports = {
5 | output: {
6 | path: join(__dirname, '../../dist/apps/api'),
7 | },
8 | plugins: [
9 | new NxWebpackPlugin({
10 | target: 'node',
11 | compiler: 'tsc',
12 | main: './src/main.ts',
13 | tsConfig: './tsconfig.app.json',
14 | assets: ['./src/assets'],
15 | optimization: false,
16 | outputHashing: 'none',
17 | }),
18 | ],
19 | };
20 |
--------------------------------------------------------------------------------
/apps/web-e2e/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:playwright/recommended", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | },
17 | {
18 | "files": ["src/**/*.{ts,js,tsx,jsx}"],
19 | "rules": {}
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/apps/web-e2e/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 | import { nxE2EPreset } from '@nx/playwright/preset';
3 |
4 | import { workspaceRoot } from '@nx/devkit';
5 |
6 | // For CI, you may want to set BASE_URL to the deployed application.
7 | const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
8 |
9 | /**
10 | * Read environment variables from file.
11 | * https://github.com/motdotla/dotenv
12 | */
13 | // require('dotenv').config();
14 |
15 | /**
16 | * See https://playwright.dev/docs/test-configuration.
17 | */
18 | export default defineConfig({
19 | ...nxE2EPreset(__filename, { testDir: './src' }),
20 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
21 | use: {
22 | baseURL,
23 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
24 | trace: 'on-first-retry',
25 | },
26 | /* Run your local dev server before starting the tests */
27 | webServer: {
28 | command: 'npx nx serve web',
29 | url: 'http://localhost:4200',
30 | reuseExistingServer: !process.env.CI,
31 | cwd: workspaceRoot,
32 | },
33 | projects: [
34 | {
35 | name: 'chromium',
36 | use: { ...devices['Desktop Chrome'] },
37 | },
38 |
39 | {
40 | name: 'firefox',
41 | use: { ...devices['Desktop Firefox'] },
42 | },
43 |
44 | {
45 | name: 'webkit',
46 | use: { ...devices['Desktop Safari'] },
47 | },
48 |
49 | // Uncomment for mobile browsers support
50 | /* {
51 | name: 'Mobile Chrome',
52 | use: { ...devices['Pixel 5'] },
53 | },
54 | {
55 | name: 'Mobile Safari',
56 | use: { ...devices['iPhone 12'] },
57 | }, */
58 |
59 | // Uncomment for branded browsers
60 | /* {
61 | name: 'Microsoft Edge',
62 | use: { ...devices['Desktop Edge'], channel: 'msedge' },
63 | },
64 | {
65 | name: 'Google Chrome',
66 | use: { ...devices['Desktop Chrome'], channel: 'chrome' },
67 | } */
68 | ],
69 | });
70 |
--------------------------------------------------------------------------------
/apps/web-e2e/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-e2e",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "projectType": "application",
5 | "sourceRoot": "apps/web-e2e/src",
6 | "targets": {},
7 | "implicitDependencies": ["web"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web-e2e/src/example.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 |
3 | test('has title', async ({ page }) => {
4 | await page.goto('/');
5 |
6 | // Expect h1 to contain a substring.
7 | expect(await page.locator('h1').innerText()).toContain('Welcome');
8 | });
9 |
--------------------------------------------------------------------------------
/apps/web-e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "outDir": "../../dist/out-tsc",
6 | "module": "commonjs",
7 | "sourceMap": false
8 | },
9 | "include": [
10 | "**/*.ts",
11 | "**/*.js",
12 | "playwright.config.ts",
13 | "src/**/*.spec.ts",
14 | "src/**/*.spec.js",
15 | "src/**/*.test.ts",
16 | "src/**/*.test.js",
17 | "src/**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@nx/react/babel",
5 | {
6 | "runtime": "automatic"
7 | }
8 | ]
9 | ],
10 | "plugins": []
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/jest.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export default {
3 | displayName: 'web',
4 | preset: '../../jest.preset.js',
5 | transform: {
6 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
7 | '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
8 | },
9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
10 | coverageDirectory: '../../coverage/apps/web',
11 | };
12 |
--------------------------------------------------------------------------------
/apps/web/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "sourceRoot": "apps/web/src",
5 | "projectType": "application",
6 | "targets": {},
7 | "tags": []
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web/src/app/app.context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export const DonasiSumContext = createContext({
5 | sum: 0,
6 | setSum: (val) => val
7 | });
8 |
9 | export const DonasiSumProvider = (props) => {
10 | // Initial values are obtained from the props
11 | const { sum: initialSum, children } = props;
12 |
13 | // Use State to keep the values
14 |
15 | const [sum, setSum] = useState(initialSum);
16 |
17 | // Make the context object:
18 | const sumContext = {
19 | sum,
20 | setSum,
21 | };
22 |
23 | // pass the value in provider and return
24 | return (
25 |
26 | {children}
27 |
28 | );
29 | };
30 |
31 | DonasiSumProvider.propTypes = {
32 | sum: PropTypes.number,
33 | };
34 |
35 | DonasiSumProvider.defaultProps = {
36 | sum: 0,
37 | };
38 |
--------------------------------------------------------------------------------
/apps/web/src/app/app.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ApolloClient,
3 | HttpLink,
4 | InMemoryCache,
5 | ApolloProvider,
6 | } from '@apollo/client';
7 | import { MantineProvider, AppShell, Footer, Header } from '@mantine/core';
8 | import { DonasiSumProvider } from './app.context';
9 | import AppFooter from './footer/footer';
10 | import AppHeader from './header/header';
11 | import AppMain from './main/main';
12 |
13 | const client = new ApolloClient({
14 | link: new HttpLink({
15 | uri: 'http://localhost:3333/graphql',
16 | }),
17 | cache: new InMemoryCache(),
18 | });
19 |
20 | export function App() {
21 | return (
22 |
23 |
24 |
25 | ({
30 | backgroundColor: theme.colors.gray[0],
31 | borderTopColor: theme.colors.gray[2],
32 | borderTopStyle: 'solid',
33 | borderTopWidth: '1px',
34 | display: 'flex',
35 | flexDirection: 'column',
36 | justifyContent: 'center',
37 | paddingTop: '4px',
38 | })}
39 | >
40 |
41 |
42 | }
43 | header={
44 | ({
47 | alignItems: 'center',
48 | backgroundColor: theme.colors.teal[9],
49 | color: '#fff',
50 | display: 'flex',
51 | fontSize: '1.8rem',
52 | lineHeight: '52px',
53 | padding: '0 16px 2px',
54 | })}
55 | >
56 |
57 |
58 | }
59 | >
60 |
61 |
62 |
63 |
64 |
65 | );
66 | }
67 |
68 | export default App;
69 |
--------------------------------------------------------------------------------
/apps/web/src/app/data-access/db.ts:
--------------------------------------------------------------------------------
1 | import { addRxPlugin, createRxDatabase } from 'rxdb';
2 | import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';
3 | import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode';
4 | import { DonasiSchema } from './donasi.schema';
5 |
6 | addRxPlugin(RxDBDevModePlugin);
7 |
8 | const createDatabase = async () => {
9 | const db = await createRxDatabase({
10 | name: 'donasidb',
11 | storage: getRxStorageDexie(),
12 | });
13 | await db.addCollections({
14 | donasi: {
15 | schema: DonasiSchema,
16 | },
17 | });
18 | return db;
19 | };
20 |
21 | let dbPromise = null;
22 |
23 | export const GetDatabase = async () => {
24 | return (dbPromise = dbPromise ?? (await createDatabase()));
25 | };
26 |
27 | export const GetDonasi = async () => {
28 | return (await GetDatabase()).donasi;
29 | };
30 |
--------------------------------------------------------------------------------
/apps/web/src/app/data-access/donasi.model.ts:
--------------------------------------------------------------------------------
1 | export interface IDonasi {
2 | id: string;
3 | createdAt: number | Date,
4 | name: string;
5 | phone: string;
6 | amount: number;
7 | sync: 0 | 1;
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web/src/app/data-access/donasi.schema.ts:
--------------------------------------------------------------------------------
1 | export const DonasiSchema = {
2 | title: 'donasi schema',
3 | version: 0,
4 | type: 'object',
5 | primaryKey: 'id',
6 | properties: {
7 | id: {
8 | type: 'string',
9 | maxLength: 10
10 | },
11 | createdAt: {
12 | type: 'number',
13 | minimum: 0,
14 | maximum: 10000000000000,
15 | multipleOf: 1
16 | },
17 | name: {
18 | type: 'string',
19 | maxLength: 100
20 | },
21 | phone: {
22 | type: 'string',
23 | },
24 | amount: {
25 | type: 'number',
26 | },
27 | syncedAt: {
28 | type: 'number',
29 | },
30 | sync: {
31 | type: 'number',
32 | default: 0
33 | },
34 | },
35 | required: ['name', 'amount'],
36 | indexes: ['name', 'createdAt'],
37 | };
38 |
--------------------------------------------------------------------------------
/apps/web/src/app/data-access/local.ts:
--------------------------------------------------------------------------------
1 | import { customAlphabet } from 'nanoid';
2 | import { useState, useEffect } from 'react';
3 | import { GetDonasi } from './db';
4 |
5 | const nanoid = customAlphabet('1234567890abcdefhijklmnopqrstuvwxyz', 10);
6 |
7 | export const FindDonasi = async (options) => {
8 | return (await GetDonasi()).find(options).exec();
9 | };
10 |
11 | export const InsertDonasi = async (values) => {
12 | values.id = values.id ?? nanoid();
13 | values.amount = +values.amount;
14 | values.createdAt = Date.now();
15 | values.sync = 0;
16 | return (await GetDonasi()).insert(values);
17 | };
18 |
19 | export const UpdateDonasi = async (id, values) => {
20 | const donasi = await GetDonasi();
21 | donasi
22 | .findOne({ selector: { $and: [{ id: { $eq: id }, sync: { $eq: 0 } }] } })
23 | .exec()
24 | .then((doc) => {
25 | doc && doc.update({ $set: { ...values } });
26 | });
27 | };
28 |
29 | export const DeleteDonasi = async (id) => {
30 | const donasi = await GetDonasi();
31 | return donasi
32 | .find({ selector: { $and: [{ id: { $eq: id }, sync: { $eq: 0 } }] } })
33 | .remove();
34 | };
35 |
36 | export function useLocalDonasiList() {
37 | const [data, setData] = useState([]);
38 | const [loading, setLoading] = useState(false);
39 | const [error, setError] = useState([]);
40 | useEffect(() => {
41 | setLoading(true);
42 | FindDonasi({
43 | selector: {},
44 | sort: [{ createdAt: 'desc' }],
45 | })
46 | .then((res) => {
47 | setData(res);
48 | })
49 | .catch((err) => {
50 | setError(err);
51 | })
52 | .finally(() => {
53 | setLoading(false);
54 | });
55 | }, []);
56 | return { data, loading, error };
57 | }
58 |
--------------------------------------------------------------------------------
/apps/web/src/app/data-access/server.ts:
--------------------------------------------------------------------------------
1 | import { gql, useLazyQuery, useMutation } from '@apollo/client';
2 | import { customAlphabet } from 'nanoid';
3 |
4 | const nanoid = customAlphabet('1234567890abcdefhijklmnopqrstuvwxyz', 10);
5 |
6 | const DonasiListGQL = gql`
7 | query DonasiListInput($filter: String, $sort: String, $order: String) {
8 | list(data: { filter: $filter, sort: $sort, order: $order }) {
9 | id
10 | createdAt
11 | name
12 | phone
13 | amount
14 | syncedAt
15 | sync
16 | }
17 | }
18 | `;
19 |
20 | export function useDonasiList() {
21 | return useLazyQuery(DonasiListGQL, {
22 | fetchPolicy: 'cache-and-network',
23 | });
24 | }
25 |
26 | export function useDonasiSearch() {
27 | return useLazyQuery(DonasiListGQL, {
28 | fetchPolicy: 'cache-and-network',
29 | });
30 | }
31 |
32 | const DonasiInsertGql = gql`
33 | mutation DonasiCreateInputs(
34 | $id: String!
35 | $createdAt: Float!
36 | $name: String!
37 | $phone: String
38 | $amount: Float!
39 | ) {
40 | create(
41 | data: {
42 | id: $id
43 | createdAt: $createdAt
44 | name: $name
45 | phone: $phone
46 | amount: $amount
47 | }
48 | ) {
49 | sync
50 | syncedAt
51 | }
52 | }
53 | `;
54 |
55 | const donasiMaper = (val) => {
56 | const { id, amount, cretedAt, ...rest } = val;
57 | return {
58 | ...rest,
59 | id: id ?? nanoid(),
60 | amount: +amount,
61 | createdAt: Date.now(),
62 | };
63 | };
64 |
65 | export function useDonasiInsert() {
66 | const [add, result] = useMutation(DonasiInsertGql);
67 | return [
68 | async (opt) => {
69 | opt.variables = donasiMaper(opt.variables);
70 | return await add(opt);
71 | },
72 | result,
73 | ];
74 | }
75 |
--------------------------------------------------------------------------------
/apps/web/src/app/footer/footer.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@mantine/core';
2 | import AddIcon from '../icons/add.svg';
3 | import HomeIcon from '../icons/home.svg';
4 | import SearchIcon from '../icons/search.svg';
5 | import SyncIcon from '../icons/sync.svg';
6 | import { Link } from './link';
7 |
8 | export function Footer() {
9 | return (
10 | ({
13 | display: 'flex',
14 | justifyContent: 'space-around',
15 | margin: '0 auto',
16 | width: '220px',
17 | })}
18 | >
19 | }>
20 | }>
21 | }>
22 | }>
23 |
24 | );
25 | }
26 |
27 | export default Footer;
28 |
--------------------------------------------------------------------------------
/apps/web/src/app/footer/link.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from '@mantine/core';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | export function Link({ to, title, icon }) {
5 | return (
6 |
17 | ({
19 | color: theme.colors.green[9],
20 | height: '24px',
21 | width: '24px',
22 | })}
23 | >
24 | {icon}
25 |
26 | ({
28 | color: theme.colors.gray[5],
29 | })}
30 | >
31 | {title}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/apps/web/src/app/header/header.tsx:
--------------------------------------------------------------------------------
1 | import { Route, Routes } from 'react-router-dom';
2 |
3 | export function Header() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 | );
12 | }
13 |
14 | export default Header;
15 |
--------------------------------------------------------------------------------
/apps/web/src/app/icons/add.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/src/app/icons/back.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/src/app/icons/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/src/app/icons/more.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/src/app/icons/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/src/app/icons/sync.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/src/app/main/add/add.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Group, LoadingOverlay, TextInput } from '@mantine/core';
2 | import { useForm } from '@mantine/form';
3 | import { useState } from 'react';
4 | import { InsertDonasi } from '../../data-access/local';
5 |
6 | function Add() {
7 | const [isLoading, setLoading] = useState(false);
8 | const form = useForm({
9 | initialValues: {
10 | name: '',
11 | phone: '',
12 | amount: 10000,
13 | },
14 | validate: {
15 | name: (value) => {
16 | return value.trim().length < 3 ? 'Required, min: 3 chars' : null;
17 | },
18 | amount: (value) => {
19 | return isNaN(value) || value < 100
20 | ? 'Numeric Required, min: 100'
21 | : null;
22 | },
23 | },
24 | });
25 |
26 | return (
27 | ({
29 | maxWidth: '300px',
30 | margin: '12px auto',
31 | })}
32 | >
33 |
77 |
78 | );
79 | }
80 |
81 | export default Add;
82 |
--------------------------------------------------------------------------------
/apps/web/src/app/main/list/list.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Tabs } from '@mantine/core';
2 | import { useState } from 'react';
3 | import LocalData from './local-data';
4 | import ServerData from './server-data';
5 | import Summary from './summary';
6 |
7 | function List() {
8 | const [selected, setSelected] = useState('server');
9 | const Comp = selected === 'server' ? ServerData : LocalData;
10 | return (
11 |
18 |
19 |
20 | Saved
21 | Draft
22 |
23 |
24 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
39 | export default List;
40 |
--------------------------------------------------------------------------------
/apps/web/src/app/main/list/local-data.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from '@mantine/core';
2 | import { useContext, useEffect } from 'react';
3 | import { DonasiSumContext } from '../../app.context';
4 | import { useLocalDonasiList } from '../../data-access/local';
5 |
6 | export function LocalData() {
7 | const { data } = useLocalDonasiList();
8 | const sumContext = useContext(DonasiSumContext);
9 | const { setSum } = sumContext;
10 | useEffect(() => {
11 | if (data.length) {
12 | setSum(data.map((d) => d.amount).reduce((a, b) => a + b, 0));
13 | }
14 | else {
15 | setSum(0);
16 | }
17 | }, [data, setSum]);
18 | return (
19 |
20 |
21 |
22 | No. |
23 | Name+(Phone) |
24 | Amount |
25 |
26 |
27 |
28 | {data.map((d, i) => (
29 |
30 | {i + 1} |
31 | {d.name} {d.phone ? ` (${d.phone})`: ""} |
32 | {d.amount} |
33 |
34 | ))}
35 |
36 |
37 | );
38 | }
39 |
40 | export default LocalData;
41 |
--------------------------------------------------------------------------------
/apps/web/src/app/main/list/server-data.tsx:
--------------------------------------------------------------------------------
1 | import { Table } from '@mantine/core';
2 | import { useContext, useEffect } from 'react';
3 | import { DonasiSumContext } from '../../app.context';
4 | import { useDonasiList } from '../../data-access/server';
5 |
6 | export function ServerData() {
7 | const [getList, { data }] = useDonasiList();
8 | const sumContext = useContext(DonasiSumContext);
9 | const { setSum } = sumContext;
10 | useEffect(() => {
11 | getList();
12 | }, [getList]);
13 | useEffect(() => {
14 | if (data) {
15 | const list = data.list;
16 | if (list.length) {
17 | setSum(list.map((d) => d.amount).reduce((a, b) => a + b, 0));
18 | } else {
19 | setSum(0);
20 | }
21 | }
22 | }, [data, setSum]);
23 | if (!data) return <> >;
24 | return (
25 |
26 |
27 |
28 | No. |
29 | Name+(Phone) |
30 | Amount |
31 |
32 |
33 |
34 | {data.list.map((d, i) => (
35 |
36 | {i + 1} |
37 | {d.name} {d.phone ? ` (${d.phone})`: ""} |
38 | {d.amount} |
39 |
40 | ))}
41 |
42 |
43 | );
44 | }
45 |
46 | export default ServerData;
47 |
--------------------------------------------------------------------------------
/apps/web/src/app/main/list/summary.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from '@mantine/core';
2 | import { useContext } from 'react';
3 | import { DonasiSumContext } from '../../app.context';
4 |
5 | export function Summary() {
6 | const { sum } = useContext(DonasiSumContext);
7 | return (
8 | ({
10 | alignItems: 'center',
11 | display: 'flex',
12 | gap: '0.5rem',
13 | justifyContent: 'center',
14 | })}
15 | >
16 | ({ fontSize: '.8em' })}>total:
17 | ({ fontSize: '.85em' })}>{sum.toLocaleString()}
18 |
19 | );
20 | }
21 |
22 | export default Summary;
23 |
--------------------------------------------------------------------------------
/apps/web/src/app/main/main.tsx:
--------------------------------------------------------------------------------
1 | import { Route, Routes } from 'react-router-dom';
2 | import Add from './add/add';
3 | import List from './list/list';
4 | import Search from './search/search';
5 | import Sync from './sync/sync';
6 |
7 | function Main() {
8 | return (
9 |
10 | } />
11 | } />
12 | } />
13 | } />
14 |
15 | );
16 | }
17 |
18 | export default Main;
19 |
--------------------------------------------------------------------------------
/apps/web/src/app/main/search/search.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 |
3 | import Search from './search';
4 |
5 | describe('Search', () => {
6 | it('should render successfully', () => {
7 | const { baseElement } = render();
8 | expect(baseElement).toBeTruthy();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/apps/web/src/app/main/search/search.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Group,
5 | LoadingOverlay,
6 | Radio,
7 | Table,
8 | TextInput,
9 | } from '@mantine/core';
10 | import { useForm } from '@mantine/form';
11 | import { useState } from 'react';
12 | import { useDonasiSearch } from '../../data-access/server';
13 |
14 | export function Search() {
15 | const [isLoading, setLoading] = useState(false);
16 | const [getList, { data }] = useDonasiSearch();
17 | const form = useForm({
18 | initialValues: {
19 | search: '',
20 | source: 'server',
21 | },
22 | validate: {
23 | search: (value) => {
24 | return value.trim().length < 3 ? 'Required, min: 3 chars' : null;
25 | },
26 | },
27 | });
28 | return (
29 | ({
31 | maxWidth: '800px',
32 | margin: '12px auto 0',
33 | })}
34 | >
35 | ({
37 | maxWidth: '300px',
38 | margin: '0 auto',
39 | })}
40 | >
41 |
78 |
79 | {data && (
80 |
81 |
82 |
83 | No. |
84 | Name+(Phone) |
85 | Amount |
86 |
87 |
88 |
89 | {data.list.map((d, i) => (
90 |
91 | {i + 1} |
92 |
93 | {d.name}
94 | {d.phone}
95 | |
96 | {d.amount.toLocaleString()} |
97 |
98 | ))}
99 |
100 |
101 | )}
102 |
103 | );
104 | }
105 |
106 | export default Search;
107 |
--------------------------------------------------------------------------------
/apps/web/src/app/main/sync/sync.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, LoadingOverlay, Table, Text } from '@mantine/core';
2 | import { useForm } from '@mantine/form';
3 | import { useReducer, useState } from 'react';
4 | import { DeleteDonasi, FindDonasi } from '../../data-access/local';
5 | import { useDonasiInsert } from '../../data-access/server';
6 |
7 | export function Sync() {
8 | const [count, setCount] = useReducer((c, cp) => (cp ? c + 1 : 0), 0);
9 | const [success, setSuccess] = useReducer(
10 | (old, val) => (val ? [...old, val] : []),
11 | []
12 | );
13 | const form = useForm({});
14 | const [isLoading, setLoading] = useState(false);
15 | const [add] = useDonasiInsert();
16 | return (
17 |
87 | );
88 | }
89 |
90 | export default Sync;
91 |
--------------------------------------------------------------------------------
/apps/web/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/web/src/assets/.gitkeep
--------------------------------------------------------------------------------
/apps/web/src/assets/icons/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/web/src/assets/icons/icon-128x128.png
--------------------------------------------------------------------------------
/apps/web/src/assets/icons/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/web/src/assets/icons/icon-144x144.png
--------------------------------------------------------------------------------
/apps/web/src/assets/icons/icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/web/src/assets/icons/icon-152x152.png
--------------------------------------------------------------------------------
/apps/web/src/assets/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/web/src/assets/icons/icon-192x192.png
--------------------------------------------------------------------------------
/apps/web/src/assets/icons/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/web/src/assets/icons/icon-384x384.png
--------------------------------------------------------------------------------
/apps/web/src/assets/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/web/src/assets/icons/icon-512x512.png
--------------------------------------------------------------------------------
/apps/web/src/assets/icons/icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/web/src/assets/icons/icon-72x72.png
--------------------------------------------------------------------------------
/apps/web/src/assets/icons/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/web/src/assets/icons/icon-96x96.png
--------------------------------------------------------------------------------
/apps/web/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/apps/web/src/favicon.ico
--------------------------------------------------------------------------------
/apps/web/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Donasi
6 |
7 |
8 |
9 |
10 |
11 |
12 |
51 |
52 |
53 |
54 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/apps/web/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react';
2 | import * as ReactDOM from 'react-dom/client';
3 | import { BrowserRouter } from 'react-router-dom';
4 |
5 | import App from './app/app';
6 |
7 | const root = ReactDOM.createRoot(
8 | document.getElementById('root') as HTMLElement
9 | );
10 | root.render(
11 |
12 |
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/apps/web/src/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "offline",
3 | "short_name": "offline",
4 | "theme_color": "#1b9aaa",
5 | "background_color": "#fafafa",
6 | "display": "standalone",
7 | "scope": "./",
8 | "start_url": "./",
9 | "icons": [
10 | {
11 | "src": "assets/icons/icon-72x72.png",
12 | "sizes": "72x72",
13 | "type": "image/png",
14 | "purpose": "maskable any"
15 | },
16 | {
17 | "src": "assets/icons/icon-96x96.png",
18 | "sizes": "96x96",
19 | "type": "image/png",
20 | "purpose": "maskable any"
21 | },
22 | {
23 | "src": "assets/icons/icon-128x128.png",
24 | "sizes": "128x128",
25 | "type": "image/png",
26 | "purpose": "maskable any"
27 | },
28 | {
29 | "src": "assets/icons/icon-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image/png",
32 | "purpose": "maskable any"
33 | },
34 | {
35 | "src": "assets/icons/icon-152x152.png",
36 | "sizes": "152x152",
37 | "type": "image/png",
38 | "purpose": "maskable any"
39 | },
40 | {
41 | "src": "assets/icons/icon-192x192.png",
42 | "sizes": "192x192",
43 | "type": "image/png",
44 | "purpose": "maskable any"
45 | },
46 | {
47 | "src": "assets/icons/icon-384x384.png",
48 | "sizes": "384x384",
49 | "type": "image/png",
50 | "purpose": "maskable any"
51 | },
52 | {
53 | "src": "assets/icons/icon-512x512.png",
54 | "sizes": "512x512",
55 | "type": "image/png",
56 | "purpose": "maskable any"
57 | }
58 | ]
59 | }
60 |
--------------------------------------------------------------------------------
/apps/web/src/serviceworker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 |
3 | const STATIC = 'precache-v1';
4 | const DYNAMIC = 'runtime';
5 |
6 | // A list of local resources we always want to be cached.
7 | const PRECACHE_URLS = [
8 | 'index.html',
9 | './', // Alias for index.html
10 | ];
11 |
12 | // The install handler takes care of precaching the resources we always need.
13 | self.addEventListener('install', (event) => {
14 | event.waitUntil(
15 | (async function () {
16 | const cache = await caches.open(STATIC);
17 | await cache.addAll(PRECACHE_URLS);
18 | })()
19 | );
20 | });
21 |
22 | // The activate handler takes care of cleaning up old caches.
23 | self.addEventListener('activate', (event) => {
24 | event.waitUntil(
25 | (async function () {
26 | const cacheNames = await caches.keys();
27 | const currentCaches = [STATIC, DYNAMIC];
28 | await Promise.all(
29 | cacheNames
30 | .filter((cacheName) => {
31 | return !currentCaches.includes(cacheName);
32 | })
33 | .map((cacheName) => caches.delete(cacheName))
34 | );
35 | })()
36 | );
37 | });
38 |
39 | // The fetch handler serves responses for same-origin resources from a cache.
40 | // If no response is found, it populates the runtime cache with the response
41 | // from the network before returning it to the page.
42 | self.addEventListener('fetch', (event) => {
43 | const req = event.request;
44 | if (req.method === 'POST' && req.url.includes('/graphql')) {
45 | graphqlHandler(event);
46 | return;
47 | }
48 | if (req.method === 'GET') {
49 | event.respondWith(
50 | (async function () {
51 | const cache = await caches.open(DYNAMIC);
52 | const cachedResponse = await cache.match(event.request);
53 | try {
54 | const networkResponse = await fetch(event.request);
55 | event.waitUntil(
56 | (async function () {
57 | await cache.put(event.request, networkResponse.clone());
58 | })()
59 | );
60 | return networkResponse;
61 | } catch (err) {
62 | return cachedResponse;
63 | }
64 | })()
65 | );
66 | return;
67 | }
68 | });
69 |
70 | function hash(x) {
71 | let h, i, l;
72 | for (h = 5381 | 0, i = 0, l = x.length | 0; i < l; i++) {
73 | h = (h << 5) + h + x.charCodeAt(i);
74 | }
75 |
76 | return h >>> 0;
77 | }
78 |
79 | async function graphqlHandler(e) {
80 | const exclude = [/mutation/, /query Identity/];
81 | const queryId = await e.request
82 | .clone()
83 | .json()
84 | .then(({ query, variables }) => {
85 | if (exclude.some((r) => r.test(query))) {
86 | return;
87 | }
88 | // Mocks a request since `caches` only works with requests.
89 | return `https://query_${hash(JSON.stringify({ query, variables }))}`;
90 | });
91 |
92 | if (!queryId) {
93 | return;
94 | }
95 |
96 | const networkResponse = fromNetwork(e.request);
97 |
98 | e.respondWith(
99 | (async () => {
100 | const cachedResult = queryId && (await fromCache(queryId));
101 | if (cachedResult) {
102 | return cachedResult;
103 | }
104 | return networkResponse.then((res) => res.clone());
105 | })()
106 | );
107 |
108 | e.waitUntil(
109 | (async () => {
110 | try {
111 | const res = await networkResponse.then((res) => res.clone());
112 | if (queryId) {
113 | await saveToCache(queryId, res);
114 | }
115 | } catch (err) {
116 | console.log(err);
117 | }
118 | })()
119 | );
120 | }
121 |
122 | async function fromCache(request) {
123 | const cache = await caches.open(DYNAMIC);
124 | const matching = await cache.match(request);
125 |
126 | return matching;
127 | }
128 |
129 | function fromNetwork(request) {
130 | return fetch(request);
131 | }
132 |
133 | async function saveToCache(request, response) {
134 | const cache = await caches.open(DYNAMIC);
135 | await cache.put(request, response);
136 | }
137 |
--------------------------------------------------------------------------------
/apps/web/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": [
6 | "node",
7 |
8 | "@nx/react/typings/cssmodule.d.ts",
9 | "@nx/react/typings/image.d.ts"
10 | ]
11 | },
12 | "exclude": [
13 | "jest.config.ts",
14 | "src/**/*.spec.ts",
15 | "src/**/*.test.ts",
16 | "src/**/*.spec.tsx",
17 | "src/**/*.test.tsx",
18 | "src/**/*.spec.js",
19 | "src/**/*.test.js",
20 | "src/**/*.spec.jsx",
21 | "src/**/*.test.jsx"
22 | ],
23 | "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
24 | }
25 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "allowJs": false,
5 | "esModuleInterop": false,
6 | "allowSyntheticDefaultImports": true,
7 | "strict": false
8 | },
9 | "files": [],
10 | "include": [],
11 | "references": [
12 | {
13 | "path": "./tsconfig.app.json"
14 | },
15 | {
16 | "path": "./tsconfig.spec.json"
17 | }
18 | ],
19 | "extends": "../../tsconfig.base.json"
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": [
7 | "jest",
8 | "node",
9 | "@nx/react/typings/cssmodule.d.ts",
10 | "@nx/react/typings/image.d.ts"
11 | ]
12 | },
13 | "include": [
14 | "jest.config.ts",
15 | "src/**/*.test.ts",
16 | "src/**/*.spec.ts",
17 | "src/**/*.test.tsx",
18 | "src/**/*.spec.tsx",
19 | "src/**/*.test.js",
20 | "src/**/*.spec.js",
21 | "src/**/*.test.jsx",
22 | "src/**/*.spec.jsx",
23 | "src/**/*.d.ts"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/apps/web/webpack.config.js:
--------------------------------------------------------------------------------
1 | const { NxWebpackPlugin } = require('@nx/webpack');
2 | const { NxReactWebpackPlugin } = require('@nx/react');
3 | const { join } = require('path');
4 |
5 | module.exports = {
6 | output: {
7 | path: join(__dirname, '../../dist/apps/web'),
8 | },
9 | devServer: {
10 | port: 4200,
11 | },
12 | plugins: [
13 | new NxWebpackPlugin({
14 | tsConfig: './tsconfig.app.json',
15 | compiler: 'babel',
16 | main: './src/main.tsx',
17 | index: './src/index.html',
18 | baseHref: '/',
19 | assets: [
20 | './src/favicon.ico',
21 | './src/manifest.webmanifest',
22 | './src/serviceworker.js',
23 | './src/assets',
24 | ],
25 | styles: ['./src/styles.css'],
26 | outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
27 | optimization: process.env['NODE_ENV'] === 'production',
28 | }),
29 | new NxReactWebpackPlugin({
30 | // Uncomment this line if you don't want to use SVGR
31 | // See: https://react-svgr.com/
32 | // svgr: false
33 | }),
34 | ],
35 | };
36 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | adminer:
4 | image: adminer:4-standalone
5 | restart: always
6 | ports:
7 | - ${ADMINER_PORT}:8080
8 | pg:
9 | image: "postgres:13-alpine"
10 | env_file:
11 | - .env
12 | ports:
13 | - ${POSTGRES_PORT}:5432
14 | volumes:
15 | - ./tmp/:/var/lib/postgresql/data/
16 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import { getJestProjects } from '@nx/jest';
2 |
3 | export default {
4 | projects: getJestProjects(),
5 | };
6 |
--------------------------------------------------------------------------------
/jest.preset.js:
--------------------------------------------------------------------------------
1 | const nxPreset = require('@nx/jest/preset').default;
2 |
3 | module.exports = { ...nxPreset };
4 |
--------------------------------------------------------------------------------
/libs/gql/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/libs/gql/README.md:
--------------------------------------------------------------------------------
1 | # gql
2 |
3 | This library was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test gql` to execute the unit tests via [Jest](https://jestjs.io).
8 |
--------------------------------------------------------------------------------
/libs/gql/jest.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export default {
3 | displayName: 'gql',
4 | preset: '../../jest.preset.js',
5 | testEnvironment: 'node',
6 | transform: {
7 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }],
8 | },
9 | moduleFileExtensions: ['ts', 'js', 'html'],
10 | coverageDirectory: '../../coverage/libs/gql',
11 | };
12 |
--------------------------------------------------------------------------------
/libs/gql/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gql",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "sourceRoot": "libs/gql/src",
5 | "projectType": "library",
6 | "targets": {},
7 | "tags": []
8 | }
9 |
--------------------------------------------------------------------------------
/libs/gql/src/gql.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
3 | import { GraphQLModule } from '@nestjs/graphql';
4 | import { join } from 'path';
5 | import { DonasiService, ServicesModule } from '@offline-first/services';
6 | import { DonasiResolver } from './resolvers/donasi.resolver';
7 |
8 | @Module({
9 | imports: [
10 | ServicesModule,
11 | GraphQLModule.forRoot({
12 | driver: ApolloDriver,
13 | autoSchemaFile: join(process.cwd(), 'prisma/schema.gql'),
14 | installSubscriptionHandlers: true,
15 | }),
16 | ],
17 | providers: [DonasiService, DonasiResolver],
18 | })
19 | export class GqlModule {}
20 |
--------------------------------------------------------------------------------
/libs/gql/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './gql.module';
2 |
--------------------------------------------------------------------------------
/libs/gql/src/models/batch-payload.ts:
--------------------------------------------------------------------------------
1 | import { ObjectType, Field } from '@nestjs/graphql';
2 |
3 | @ObjectType()
4 | export class BatchPayload {
5 | @Field()
6 | count: number;
7 | }
8 |
--------------------------------------------------------------------------------
/libs/gql/src/models/donasi-create-input.ts:
--------------------------------------------------------------------------------
1 | import { InputType, Field } from '@nestjs/graphql';
2 |
3 | @InputType()
4 | export class DonasiCreateInput {
5 | @Field()
6 | createdAt: number;
7 |
8 | @Field(() => String)
9 | id: string;
10 |
11 | @Field(() => String)
12 | name?: string | null;
13 |
14 | @Field(() => String, { nullable: true })
15 | phone?: string | null;
16 |
17 | @Field()
18 | amount: number;
19 | }
20 |
--------------------------------------------------------------------------------
/libs/gql/src/models/donasi-list-input.ts:
--------------------------------------------------------------------------------
1 | import { InputType, Field } from '@nestjs/graphql';
2 |
3 | @InputType()
4 | export class DonasiListInput {
5 | @Field(() => String, { nullable: true })
6 | filter?: string | null;
7 |
8 | @Field(() => String, { nullable: true })
9 | sort?: string | null;
10 |
11 | @Field(() => String, { nullable: true })
12 | order?: string | null;
13 | }
14 |
--------------------------------------------------------------------------------
/libs/gql/src/models/donasi-pagelist-input.ts:
--------------------------------------------------------------------------------
1 | import { InputType, Field } from '@nestjs/graphql';
2 |
3 | @InputType()
4 | export class DonasiPagelistInput {
5 | @Field()
6 | take: number;
7 |
8 | @Field()
9 | skip: number;
10 |
11 | @Field(() => String, { nullable: true })
12 | filter?: string | null;
13 |
14 | @Field(() => String, { nullable: true })
15 | sort?: string | null;
16 |
17 | @Field(() => String, { nullable: true })
18 | order?: string | null;
19 | }
20 |
--------------------------------------------------------------------------------
/libs/gql/src/models/donasi.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata'
2 | import { ObjectType, Field, ID } from '@nestjs/graphql';
3 |
4 | @ObjectType()
5 | export class Donasi {
6 | @Field(() => ID)
7 | sid: number
8 |
9 | @Field()
10 | id: string
11 |
12 | @Field()
13 | createdAt: Date
14 |
15 | @Field(() => String, { nullable: true })
16 | name?: string | null
17 |
18 | @Field(() => String, { nullable: true })
19 | phone?: string | null
20 |
21 | @Field()
22 | amount: number
23 |
24 | @Field(() => Date, { nullable: true })
25 | syncedAt?: Date | null
26 |
27 | @Field()
28 | sync: boolean
29 | }
30 |
--------------------------------------------------------------------------------
/libs/gql/src/resolvers/donasi.resolver.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 | import {
3 | Resolver,
4 | Query,
5 | Mutation,
6 | Args,
7 | InputType,
8 | Field,
9 | } from '@nestjs/graphql';
10 | import { Inject } from '@nestjs/common';
11 | import { DonasiService } from '@offline-first/services';
12 | import { Donasi } from '../models/donasi';
13 | import { DonasiCreateInput } from '../models/donasi-create-input';
14 | import { BatchPayload } from '../models/batch-payload';
15 | import { DonasiListInput } from '../models/donasi-list-input';
16 | import { DonasiPagelistInput } from '../models/donasi-pagelist-input';
17 |
18 | @InputType()
19 | class DonasiCreateInputs {
20 | @Field(() => [DonasiCreateInput])
21 | batch: DonasiCreateInput[];
22 | }
23 |
24 | const donasiMapper = (data: DonasiCreateInput) => {
25 | const { createdAt, ...rest } = data;
26 | return {
27 | ...rest,
28 | createdAt: new Date(createdAt),
29 | };
30 | };
31 |
32 | @Resolver(Donasi)
33 | export class DonasiResolver {
34 | constructor(@Inject(DonasiService) private donasiService: DonasiService) {}
35 |
36 | @Mutation(() => Donasi)
37 | async create(@Args('data') data: DonasiCreateInput): Promise {
38 | return this.donasiService.create(donasiMapper(data));
39 | }
40 |
41 | @Mutation(() => BatchPayload)
42 | async createMany(
43 | @Args('data') data: DonasiCreateInputs
44 | ): Promise {
45 | const dataFixed = data.batch.map((val) => donasiMapper(val));
46 | return this.donasiService.createMany(dataFixed);
47 | }
48 |
49 | @Query(() => Donasi, { nullable: true })
50 | async oneById(@Args('id') sid: number) {
51 | return this.donasiService.oneById({ sid });
52 | }
53 |
54 | @Query(() => [Donasi])
55 | async pagelist(@Args('data') data: DonasiPagelistInput) {
56 | const { skip, take, filter } = data;
57 | let { sort, order } = data;
58 | sort = sort ?? 'syncedAt';
59 | order = order ?? 'desc';
60 | const orderBy = { [sort]: order };
61 | let where = {};
62 | if (filter) {
63 | where = {
64 | OR: [{ name: { contains: filter } }, { phone: { contains: filter } }],
65 | };
66 | }
67 | return this.donasiService.pagelist({
68 | skip,
69 | take,
70 | where,
71 | orderBy,
72 | });
73 | }
74 |
75 | @Query(() => [Donasi])
76 | async list(@Args('data') data: DonasiListInput) {
77 | let { sort, order } = data;
78 | sort = sort ?? 'syncedAt';
79 | order = order ?? 'desc';
80 | const orderBy = { [sort]: order };
81 | const filter = data.filter;
82 | let where = {};
83 | if (filter) {
84 | where = {
85 | OR: [{ name: { contains: filter } }, { phone: { contains: filter } }],
86 | };
87 | }
88 | return this.donasiService.pagelist({
89 | where,
90 | orderBy,
91 | });
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/libs/gql/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "forceConsistentCasingInFileNames": true,
6 | "strict": false,
7 | "noImplicitOverride": true,
8 | "noPropertyAccessFromIndexSignature": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true
11 | },
12 | "files": [],
13 | "include": [],
14 | "references": [
15 | {
16 | "path": "./tsconfig.lib.json"
17 | },
18 | {
19 | "path": "./tsconfig.spec.json"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/libs/gql/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "declaration": true,
6 | "types": ["node"],
7 | "target": "es2021",
8 | "strictNullChecks": true,
9 | "noImplicitAny": true,
10 | "strictBindCallApply": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noFallthroughCasesInSwitch": true
13 | },
14 | "include": ["src/**/*.ts"],
15 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
16 | }
17 |
--------------------------------------------------------------------------------
/libs/gql/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": [
9 | "jest.config.ts",
10 | "src/**/*.test.ts",
11 | "src/**/*.spec.ts",
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/libs/services/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.js", "*.jsx"],
15 | "rules": {}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/libs/services/README.md:
--------------------------------------------------------------------------------
1 | # services
2 |
3 | This library was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test services` to execute the unit tests via [Jest](https://jestjs.io).
8 |
--------------------------------------------------------------------------------
/libs/services/jest.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export default {
3 | displayName: 'services',
4 | preset: '../../jest.preset.js',
5 | testEnvironment: 'node',
6 | transform: {
7 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }],
8 | },
9 | moduleFileExtensions: ['ts', 'js', 'html'],
10 | coverageDirectory: '../../coverage/libs/services',
11 | };
12 |
--------------------------------------------------------------------------------
/libs/services/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "services",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "sourceRoot": "libs/services/src",
5 | "projectType": "library",
6 | "targets": {},
7 | "tags": []
8 | }
9 |
--------------------------------------------------------------------------------
/libs/services/src/donasi/donasi.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { Donasi, Prisma } from '@prisma/client';
3 | import { PrismaService } from '../prisma/prisma.service';
4 |
5 | @Injectable()
6 | export class DonasiService {
7 | constructor(private prisma: PrismaService) {}
8 |
9 | async oneById(byId: Prisma.DonasiWhereUniqueInput): Promise {
10 | return this.prisma.donasi.findUnique({
11 | where: byId,
12 | });
13 | }
14 |
15 | async pagelist(params: {
16 | skip?: number;
17 | take?: number;
18 | cursor?: Prisma.DonasiWhereUniqueInput;
19 | where?: Prisma.DonasiWhereInput;
20 | orderBy?: Prisma.DonasiOrderByWithRelationInput;
21 | }): Promise {
22 | const { skip, take, cursor, where, orderBy } = params;
23 | return this.prisma.donasi.findMany({
24 | skip,
25 | take,
26 | cursor,
27 | where,
28 | orderBy,
29 | });
30 | }
31 |
32 | async create(data: Prisma.DonasiCreateInput): Promise {
33 | return this.prisma.donasi.create({
34 | data,
35 | });
36 | }
37 |
38 | async createMany(
39 | data: Prisma.DonasiCreateInput[]
40 | ): Promise {
41 | return await this.prisma.donasi.createMany({
42 | data,
43 | skipDuplicates: true,
44 | });
45 | }
46 |
47 | async update(params: {
48 | where: Prisma.DonasiWhereUniqueInput;
49 | data: Prisma.DonasiUpdateInput;
50 | }): Promise {
51 | const { where, data } = params;
52 | return this.prisma.donasi.update({
53 | data,
54 | where,
55 | });
56 | }
57 |
58 | async deleteUser(where: Prisma.DonasiWhereUniqueInput): Promise {
59 | return this.prisma.donasi.delete({
60 | where,
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/libs/services/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './services.module';
2 | export * from './donasi/donasi.service';
3 |
--------------------------------------------------------------------------------
/libs/services/src/prisma/prisma.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
2 | import { PrismaClient } from '@prisma/client';
3 |
4 | @Injectable()
5 | export class PrismaService
6 | extends PrismaClient
7 | implements OnModuleInit, OnModuleDestroy {
8 | async onModuleInit() {
9 | await this.$connect();
10 | }
11 |
12 | async onModuleDestroy() {
13 | await this.$disconnect();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/libs/services/src/services.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { PrismaService } from './prisma/prisma.service';
3 | import { DonasiService } from './donasi/donasi.service';
4 |
5 | @Module({
6 | controllers: [],
7 | providers: [PrismaService, DonasiService],
8 | exports: [PrismaService, DonasiService],
9 | })
10 | export class ServicesModule {}
11 |
--------------------------------------------------------------------------------
/libs/services/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "forceConsistentCasingInFileNames": true,
6 | "strict": true,
7 | "noImplicitOverride": true,
8 | "noPropertyAccessFromIndexSignature": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true
11 | },
12 | "files": [],
13 | "include": [],
14 | "references": [
15 | {
16 | "path": "./tsconfig.lib.json"
17 | },
18 | {
19 | "path": "./tsconfig.spec.json"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/libs/services/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "declaration": true,
6 | "types": ["node"],
7 | "target": "es2021",
8 | "strictNullChecks": true,
9 | "noImplicitAny": true,
10 | "strictBindCallApply": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noFallthroughCasesInSwitch": true
13 | },
14 | "include": ["src/**/*.ts"],
15 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
16 | }
17 |
--------------------------------------------------------------------------------
/libs/services/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": [
9 | "jest.config.ts",
10 | "src/**/*.test.ts",
11 | "src/**/*.spec.ts",
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/nx/schemas/nx-schema.json",
3 | "namedInputs": {
4 | "default": ["{projectRoot}/**/*", "sharedGlobals"],
5 | "production": [
6 | "default",
7 | "!{projectRoot}/.eslintrc.json",
8 | "!{projectRoot}/eslint.config.js",
9 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
10 | "!{projectRoot}/tsconfig.spec.json",
11 | "!{projectRoot}/jest.config.[jt]s",
12 | "!{projectRoot}/src/test-setup.[jt]s",
13 | "!{projectRoot}/test-setup.[jt]s"
14 | ],
15 | "sharedGlobals": []
16 | },
17 | "plugins": [
18 | {
19 | "plugin": "@nx/webpack/plugin",
20 | "options": {
21 | "buildTargetName": "build",
22 | "serveTargetName": "serve",
23 | "previewTargetName": "preview"
24 | }
25 | },
26 | {
27 | "plugin": "@nx/eslint/plugin",
28 | "options": {
29 | "targetName": "lint"
30 | }
31 | },
32 | {
33 | "plugin": "@nx/jest/plugin",
34 | "options": {
35 | "targetName": "test"
36 | }
37 | },
38 | {
39 | "plugin": "@nx/playwright/plugin",
40 | "options": {
41 | "targetName": "e2e"
42 | }
43 | }
44 | ],
45 | "generators": {
46 | "@nx/react": {
47 | "application": {
48 | "babel": true,
49 | "style": "css",
50 | "linter": "eslint",
51 | "bundler": "webpack"
52 | },
53 | "component": {
54 | "style": "css"
55 | },
56 | "library": {
57 | "style": "css",
58 | "linter": "eslint"
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@offline-first3/source",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "nx": "nx",
7 | "start": "nx serve",
8 | "build": "nx build",
9 | "test": "nx test",
10 | "lint": "nx workspace-lint && nx lint",
11 | "e2e": "nx e2e",
12 | "affected:apps": "nx affected:apps",
13 | "affected:libs": "nx affected:libs",
14 | "affected:build": "nx affected:build",
15 | "affected:e2e": "nx affected:e2e",
16 | "affected:test": "nx affected:test",
17 | "affected:lint": "nx affected:lint",
18 | "affected:dep-graph": "nx affected:dep-graph",
19 | "affected": "nx affected",
20 | "format": "nx format:write",
21 | "format:write": "nx format:write",
22 | "format:check": "nx format:check",
23 | "update": "nx migrate latest",
24 | "workspace-generator": "nx workspace-generator",
25 | "dep-graph": "nx dep-graph",
26 | "help": "nx help",
27 | "migrate:dev": "prisma migrate dev --preview-feature",
28 | "migrate:dev:create": "prisma migrate dev --create-only --preview-feature",
29 | "migrate:reset": "prisma migrate reset --preview-feature",
30 | "migrate:deploy": "npx prisma migrate deploy --preview-feature",
31 | "migrate:status": "npx prisma migrate status --preview-feature",
32 | "migrate:resolve": "npx prisma migrate resolve --preview-feature",
33 | "prisma:studio": "npx prisma studio",
34 | "prisma:generate": "npx prisma generate",
35 | "prisma:generate:watch": "npx prisma generate --watch"
36 | },
37 | "private": true,
38 | "devDependencies": {
39 | "@babel/core": "^7.14.5",
40 | "@babel/preset-react": "^7.14.5",
41 | "@nestjs/schematics": "^10.0.1",
42 | "@nestjs/testing": "^10.0.2",
43 | "@nx/devkit": "18.0.7",
44 | "@nx/eslint": "18.0.7",
45 | "@nx/eslint-plugin": "18.0.7",
46 | "@nx/jest": "18.0.7",
47 | "@nx/js": "18.0.7",
48 | "@nx/nest": "^18.0.7",
49 | "@nx/node": "18.0.7",
50 | "@nx/playwright": "18.0.7",
51 | "@nx/react": "^18.0.7",
52 | "@nx/web": "18.0.7",
53 | "@nx/webpack": "18.0.7",
54 | "@nx/workspace": "18.0.7",
55 | "@playwright/test": "^1.36.0",
56 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
57 | "@svgr/webpack": "^8.0.1",
58 | "@swc-node/register": "~1.8.0",
59 | "@swc/cli": "~0.1.62",
60 | "@swc/core": "~1.3.85",
61 | "@swc/helpers": "~0.5.2",
62 | "@testing-library/react": "14.0.0",
63 | "@types/jest": "^29.4.0",
64 | "@types/node": "~18.16.9",
65 | "@types/react": "18.2.33",
66 | "@types/react-dom": "18.2.14",
67 | "@typescript-eslint/eslint-plugin": "^6.13.2",
68 | "@typescript-eslint/parser": "^6.13.2",
69 | "babel-jest": "^29.4.1",
70 | "eslint": "~8.48.0",
71 | "eslint-config-prettier": "^9.0.0",
72 | "eslint-plugin-import": "2.27.5",
73 | "eslint-plugin-jsx-a11y": "6.7.1",
74 | "eslint-plugin-playwright": "^0.15.3",
75 | "eslint-plugin-react": "7.32.2",
76 | "eslint-plugin-react-hooks": "4.6.0",
77 | "jest": "^29.4.1",
78 | "jest-environment-jsdom": "^29.4.1",
79 | "jest-environment-node": "^29.4.1",
80 | "nx": "18.0.7",
81 | "prettier": "^2.6.2",
82 | "prisma": "^5.10.2",
83 | "react-refresh": "^0.10.0",
84 | "ts-jest": "^29.1.0",
85 | "ts-node": "10.9.1",
86 | "typescript": "~5.3.2",
87 | "url-loader": "^4.1.1",
88 | "webpack-cli": "^5.1.4"
89 | },
90 | "dependencies": {
91 | "@apollo/client": "^3.9.5",
92 | "@apollo/server": "^4.10.0",
93 | "@emotion/react": "^11.11.4",
94 | "@mantine/core": "^6.0.21",
95 | "@mantine/form": "^6.0.21",
96 | "@nestjs/apollo": "^12.1.0",
97 | "@nestjs/common": "^10.0.2",
98 | "@nestjs/config": "^3.2.0",
99 | "@nestjs/core": "^10.0.2",
100 | "@nestjs/graphql": "^12.1.1",
101 | "@nestjs/platform-express": "^10.0.2",
102 | "@prisma/client": "^5.10.2",
103 | "axios": "^1.6.0",
104 | "graphql": "^16.8.1",
105 | "graphql-tools": "^9.0.1",
106 | "nanoid": "^5.0.6",
107 | "react": "18.2.0",
108 | "react-dom": "18.2.0",
109 | "react-router-dom": "6.11.2",
110 | "reflect-metadata": "^0.1.13",
111 | "rxdb": "^15.10.0",
112 | "rxjs": "^7.8.0",
113 | "tslib": "^2.3.0"
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | datasource db {
5 | provider = "postgresql"
6 | url = env("DATABASE_URL")
7 | }
8 |
9 | generator client {
10 | provider = "prisma-client-js"
11 | }
12 |
13 | model Donasi {
14 | sid Int @id @default(autoincrement())
15 | id String @unique
16 | createdAt DateTime
17 | name String?
18 | phone String?
19 | amount Int
20 | syncedAt DateTime @default(now())
21 | sync Boolean @default(true)
22 | }
23 |
--------------------------------------------------------------------------------
/screenshot/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/screenshot/1.png
--------------------------------------------------------------------------------
/screenshot/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madipta/offline-first/3ca3fe0a205c7ec9099cce23bc52f28d16be1ac2/screenshot/2.png
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "importHelpers": true,
11 | "target": "es2015",
12 | "module": "esnext",
13 | "lib": ["es2020", "dom"],
14 | "skipLibCheck": true,
15 | "skipDefaultLibCheck": true,
16 | "baseUrl": ".",
17 | "paths": {
18 | "@offline-first/gql": ["libs/gql/src/index.ts"],
19 | "@offline-first/services": ["libs/services/src/index.ts"]
20 | }
21 | },
22 | "exclude": ["node_modules", "tmp"]
23 | }
24 |
--------------------------------------------------------------------------------