diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..a067bcf --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,41 @@ +{ + "name": "lockerai", + "workspaceFolder": "/workspaces/lockerai/", + "dockerComposeFile": ["../docker/docker-compose.development.yaml"], + "service": "app", + "customizations": { + "vscode": { + "extensions": [ + "adam-bender.commit-message-editor", + "bierner.color-info", + "bradlc.vscode-tailwindcss", + "christian-kohler.path-intellisense", + "dbaeumer.vscode-eslint", + "eamodio.gitlens", + "ecmel.vscode-html-css", + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "figma.figma-vscode-extension", + "formulahendry.auto-complete-tag", + "github.copilot", + "github.copilot-labs", + "github.vscode-github-actions", + "github.vscode-pull-request-github", + "graphql.vscode-graphql", + "gruntfuggly.todo-tree", + "hbenl.vscode-test-explorer", + "jock.svg", + "mikestead.dotenv", + "ms-ossdata.vscode-postgresql", + "orta.vscode-jest", + "stylelint.vscode-stylelint", + "usernamehw.errorlens", + "vincaslt.highlight-matching-tag", + "visualstudioexptteam.vscodeintellicode", + "wix.vscode-import-cost", + "wmaurer.change-case", + "yoavbls.pretty-ts-errors" + ] + } + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1231c1f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +# debug +**/*.log + +# deliverable +**/.next/ +**/build/ +**/dist/ +**/out/ + +# dependency +**/node_modules/ +**/.pnpm-store/ + +# env file +**/.env* +!**/.env.example + +# storybook +**/.storybook/static/ + +# testing +**/coverage + +# typescript +**/*.tsbuildinfo diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a7ea30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ddc700a --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +COMPOSE_PROJECT_NAME="lockerai" +SUPABASE_ACCESS_TOKEN="" +SUPABASE_ANON_KEY="" +SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID="" +SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET="" +SUPABASE_AUTH_EXTERNAL_GOOGLE_URL="" +SUPABASE_JWT_SECRET="" +SUPABASE_REFERENCE_ID="" +SUPABASE_SERVICE_ROLE_KEY="" +TURBO_TEAM="" +TURBO_TOKEN="" diff --git a/.github/filter.yaml b/.github/filter.yaml new file mode 100644 index 0000000..9037e10 --- /dev/null +++ b/.github/filter.yaml @@ -0,0 +1,55 @@ +api: + - .github/filter.yaml + - .github/workflows/api-*.yaml + - apps/api/**/* + - packages/**/* + - .npmrc + - package.json + - pnpm-workspace.yaml + - turbo.json + +app: + - .github/filter.yaml + - .github/workflows/app-*.yaml + - apps/**/* + - packages/**/* + - .npmrc + - package.json + - pnpm-workspace.yaml + - turbo.json + +catalog: + - .github/filter.yaml + - .github/workflows/catalog-*.yaml + - apps/catalog/**/* + - apps/website/**/* + - packages/**/* + - .npmrc + - package.json + - pnpm-workspace.yaml + - turbo.json + +db: + - .github/filter.yaml + - .github/workflows/db-*.yaml + - apps/api/src/infra/drizzle/schema/**/*.ts + +graphql: + - .github/filter.yaml + - .github/workflows/graphql-*.yaml + - apps/api/**/* + - packages/**/* + - .npmrc + - package.json + - pnpm-workspace.yaml + - turbo.json + +website: + - .github/filter.yaml + - .github/workflows/website-*.yaml + - apps/website/**/* + - packages/**/* + - .npmrc + - package.json + - pnpm-workspace.yaml + - turbo.json diff --git a/.github/workflows/app-test.yaml b/.github/workflows/app-test.yaml new file mode 100644 index 0000000..6f6d2ee --- /dev/null +++ b/.github/workflows/app-test.yaml @@ -0,0 +1,234 @@ +name: app test + +on: push + +env: + DATABASE_URL: postgresql://postgres:postgres@localhost:54322/postgres + NEXT_PUBLIC_GRAPHQL_ENDPOINT: ${{ secrets.NEXT_PUBLIC_GRAPHQL_ENDPOINT }} + NEXT_PUBLIC_WS_ENDPOINT: ${{ secrets.NEXT_PUBLIC_WS_ENDPOINT }} + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + +jobs: + filter: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + app: ${{ steps.changes.outputs.app }} + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: check for file changes + uses: dorny/paths-filter@v2 + id: changes + with: + token: ${{ github.token }} + filters: .github/filter.yaml + + install: + runs-on: ubuntu-latest + needs: filter + if: needs.filter.outputs.app == 'true' + outputs: + STORE_PATH: ${{ steps.pnpm-env.outputs.STORE_PATH }} + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version-file: "package.json" + + - name: setup pnpm + uses: pnpm/action-setup@v2 + with: + run_install: false + + - name: setup pnpm environment variable + id: pnpm-env + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: cache pnpm dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-env.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-dependencies-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-dependencies- + + - name: install dependencies + run: pnpm install --frozen-lockfile + + format: + runs-on: ubuntu-latest + needs: install + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version-file: "package.json" + + - name: setup pnpm + uses: pnpm/action-setup@v2 + id: pnpm-install + with: + run_install: false + + - name: restore pnpm dependencies + uses: actions/cache/restore@v3 + with: + path: ${{ needs.install.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-dependencies-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-dependencies- + + - name: install dependencies + run: pnpm install --frozen-lockfile + + - name: format + run: pnpm turbo run fmt + + lint: + runs-on: ubuntu-latest + needs: install + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version-file: "package.json" + + - name: setup pnpm + uses: pnpm/action-setup@v2 + id: pnpm-install + with: + run_install: false + + - name: restore pnpm dependencies + uses: actions/cache/restore@v3 + with: + path: ${{ needs.install.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-dependencies-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-dependencies- + + - name: install + run: pnpm install --frozen-lockfile + + - name: lint + run: pnpm turbo run lint -- --max-warnings=0 + + style-lint: + runs-on: ubuntu-latest + needs: install + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version-file: "package.json" + + - name: setup pnpm + uses: pnpm/action-setup@v2 + id: pnpm-install + with: + run_install: false + + - name: restore pnpm dependencies + uses: actions/cache/restore@v3 + with: + path: ${{ needs.install.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-dependencies-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-dependencies- + + - name: install dependencies + run: pnpm install --frozen-lockfile + + - name: lint styles + run: pnpm turbo run style -- --max-warnings=0 + + build: + runs-on: ubuntu-latest + needs: install + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version-file: "package.json" + + - name: setup pnpm + uses: pnpm/action-setup@v2 + id: pnpm-install + with: + run_install: false + + - name: restore pnpm dependencies + uses: actions/cache/restore@v3 + with: + path: ${{ needs.install.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-dependencies-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-dependencies- + + - name: install dependencies + run: pnpm install --frozen-lockfile + + - name: build + run: pnpm turbo run build + + test: + runs-on: ubuntu-latest + needs: [install, build] + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup supabase cli + uses: supabase/setup-cli@v1 + with: + version: latest + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version-file: "package.json" + + - name: setup pnpm + uses: pnpm/action-setup@v2 + id: pnpm-install + with: + run_install: false + + - name: restore pnpm dependencies + uses: actions/cache/restore@v3 + with: + path: ${{ needs.install.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-dependencies-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-dependencies- + + - name: install dependencies + run: pnpm install --frozen-lockfile + + - name: authenticate to supabase + run: pnpm supabase link --project-ref ${{ secrets.SUPABASE_REFERENCE_ID }} --password ${{ secrets.SUPABASE_DATABASE_PASSWORD }} + + - name: test + run: pnpm turbo run test + + app-test-check: + runs-on: ubuntu-latest + needs: [format, lint, style-lint, build, test] + if: ${{ ! failure() }} + steps: + - name: check + run: echo "test is successfully executed." diff --git a/.github/workflows/catalog-deploy-chromatic.yaml b/.github/workflows/catalog-deploy-chromatic.yaml new file mode 100644 index 0000000..cd4a3e0 --- /dev/null +++ b/.github/workflows/catalog-deploy-chromatic.yaml @@ -0,0 +1,81 @@ +name: app test + +on: push + +env: + DATABASE_URL: postgresql://postgres:postgres@localhost:54322/postgres + NEXT_PUBLIC_GRAPHQL_ENDPOINT: ${{ secrets.NEXT_PUBLIC_GRAPHQL_ENDPOINT }} + NEXT_PUBLIC_WS_ENDPOINT: ${{ secrets.NEXT_PUBLIC_WS_ENDPOINT }} + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + +jobs: + filter: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + catalog: ${{ steps.changes.outputs.catalog }} + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: check for file changes + uses: dorny/paths-filter@v2 + id: changes + with: + token: ${{ github.token }} + filters: .github/filter.yaml + + deploy-chromatic: + runs-on: ubuntu-latest + needs: filter + if: needs.filter.outputs.catalog == 'true' + steps: + - name: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha }} + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version-file: "package.json" + + - name: setup pnpm + uses: pnpm/action-setup@v2 + id: pnpm-install + with: + run_install: false + + - name: setup pnpm environment variable + id: pnpm-env + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: cache pnpm dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-env.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-dependencies-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-dependencies- + + - name: install dependencies + run: pnpm install --frozen-lockfile + + - name: publish storybook + uses: chromaui/action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + workingDir: apps/catalog/ + buildScriptName: sb:build + + app-test-check: + runs-on: ubuntu-latest + needs: deploy-chromatic + if: ${{ ! failure() }} + steps: + - name: check + run: echo "test is successfully executed." diff --git a/.github/workflows/db-commit-migration.yaml b/.github/workflows/db-commit-migration.yaml new file mode 100644 index 0000000..bce8058 --- /dev/null +++ b/.github/workflows/db-commit-migration.yaml @@ -0,0 +1,111 @@ +name: db commit migration + +on: push + +env: + DATABASE_URL: postgresql://postgres:postgres@localhost:54322/postgres + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + +jobs: + filter: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + db: ${{ steps.changes.outputs.db }} + steps: + - name: checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + + - name: check for file changes + uses: dorny/paths-filter@v2 + id: changes + with: + token: ${{ github.token }} + filters: .github/filter.yaml + + commit: + runs-on: ubuntu-latest + needs: filter + if: needs.filter.outputs.db == 'true' + steps: + - name: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha }} + token: ${{ secrets.PAT }} + + - name: setup supabase cli + uses: supabase/setup-cli@v1 + with: + version: latest + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version-file: "package.json" + + - name: setup pnpm + uses: pnpm/action-setup@v2 + id: pnpm-install + with: + run_install: false + + - name: setup pnpm environment variable + id: pnpm-env + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: cache pnpm dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-env.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-dependencies-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-dependencies- + + - name: install dependencies + run: pnpm install --frozen-lockfile + + - name: authenticate to supabase + run: pnpm supabase link --project-ref ${{ secrets.SUPABASE_REFERENCE_ID }} --password ${{ secrets.SUPABASE_DATABASE_PASSWORD }} + + - name: update migration file + run: echo y | (supabase db pull --schema public && supabase migration squash) + continue-on-error: true + + - name: commit migration file update + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + if git diff --name-only --diff-filter=D | grep -q "supabase/migrations/.*_remote_schema.sql"; then \ + git add $(git diff --name-only --diff-filter=D); \ + OLD_VERSION=$(git diff --name-only --diff-filter=D --staged | grep "supabase/migrations/.*_remote_schema.sql" | cut -d/ -f3 | cut -d_ -f1); \ + git add supabase/migrations/*_remote_schema.sql -N; \ + NEW_VERSION=$(git diff --name-only --diff-filter=A | grep "supabase/migrations/.*_remote_schema.sql" | cut -d/ -f3 | cut -d_ -f1); \ + pnpm supabase migration repair ${OLD_VERSION} --status reverted; \ + pnpm supabase migration repair ${NEW_VERSION} --status reverted; \ + pnpm supabase migration repair ${NEW_VERSION} --status applied; \ + git add supabase/migrations/*_remote_schema.sql; \ + git commit -m "actions: ๐Ÿค– (supabase) updated migration file"; \ + git push origin HEAD:${GITHUB_REF} --force; \ + echo "Committed changes to migration history"; \ + elif grep -q "supabase/migrations/.*_remote_schema.sql"; then \ + git add supabase/migrations/*_remote_schema.sql; \ + git commit -m "actions: ๐Ÿค– (supabase) created migration file"; \ + git push origin HEAD:${GITHUB_REF} --force; \ + echo "Committed changes to migration history"; \ + else \ + echo "No changes detected in migration history"; \ + fi + + db-commit-migration-check: + runs-on: ubuntu-latest + needs: commit + if: ${{ ! failure() }} + steps: + - name: check + run: echo "test is successfully executed." diff --git a/.github/workflows/graphql-commit-schema.yaml b/.github/workflows/graphql-commit-schema.yaml new file mode 100644 index 0000000..dfcb0c9 --- /dev/null +++ b/.github/workflows/graphql-commit-schema.yaml @@ -0,0 +1,91 @@ +name: graphql commit schema + +on: push + +env: + DATABASE_URL: postgresql://postgres:postgres@localhost:54322/postgres + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + +jobs: + filter: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + graphql: ${{ steps.changes.outputs.graphql }} + steps: + - name: checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + + - name: check for file changes + uses: dorny/paths-filter@v2 + id: changes + with: + token: ${{ github.token }} + filters: .github/filter.yaml + + commit: + runs-on: ubuntu-latest + needs: filter + if: needs.filter.outputs.graphql == 'true' + steps: + - name: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.workflow_run.head_sha }} + token: ${{ secrets.PAT }} + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version-file: "package.json" + + - name: setup pnpm + uses: pnpm/action-setup@v2 + id: pnpm-install + with: + run_install: false + + - name: setup pnpm environment variable + id: pnpm-env + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: cache pnpm dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-env.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-dependencies-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-dependencies- + + - name: install dependencies + run: pnpm install --frozen-lockfile + + - name: generate and update graphql schema + run: seq 2 | timeout 30 xargs -P 2 -I {} sh .github/workflows/script/graphql-commit-schema.sh {} + continue-on-error: true + + - name: commit graphql schema update + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + if git diff --name-only | grep -q "apps/website/graphql.schema.json"; then \ + git add ./apps/website/graphql.schema.json; \ + git commit -m "actions: ๐Ÿค– (graphql) updated graphql schema"; \ + git push origin HEAD:${GITHUB_REF} --force; \ + echo "Committed changes to GraphQL schema"; \ + else \ + echo "No changes detected in GraphQL schema"; \ + fi + + graphql-commit-schema-check: + runs-on: ubuntu-latest + needs: commit + if: ${{ ! failure() }} + steps: + - name: check + run: echo "test is successfully executed." diff --git a/.github/workflows/graphql-deploy-apollo-studio.yaml b/.github/workflows/graphql-deploy-apollo-studio.yaml new file mode 100644 index 0000000..2d151ab --- /dev/null +++ b/.github/workflows/graphql-deploy-apollo-studio.yaml @@ -0,0 +1,80 @@ +name: graphql deploy apollo studio + +on: + push: + branches: + - main + +env: + DATABASE_URL: postgresql://postgres:postgres@localhost:54322/postgres + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} + +jobs: + filter: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + graphql: ${{ steps.changes.outputs.graphql }} + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: check for file changes + uses: dorny/paths-filter@v2 + id: changes + with: + token: ${{ github.token }} + filters: .github/filter.yaml + + deploy-apollo-studio: + runs-on: ubuntu-latest + env: + APOLLO_KEY: ${{ secrets.APOLLO_KEY }} + needs: filter + if: needs.filter.outputs.graphql == 'true' + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version-file: "package.json" + + - name: setup pnpm + uses: pnpm/action-setup@v2 + id: pnpm-install + with: + run_install: false + + - name: setup pnpm environment variable + id: pnpm-env + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: cache pnpm dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-env.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-dependencies-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-dependencies- + + - name: install dependencies + run: pnpm install --frozen-lockfile + + - name: generate graphql schema + run: timeout 15 pnpm api dev + continue-on-error: true + + - name: publish graphs + run: pnpm api apollo:rover + + app-test-check: + runs-on: ubuntu-latest + needs: deploy-apollo-studio + if: ${{ ! failure() }} + steps: + - name: check + run: echo "test is successfully executed." diff --git a/.github/workflows/script/graphql-commit-schema.sh b/.github/workflows/script/graphql-commit-schema.sh new file mode 100644 index 0000000..a5b91f1 --- /dev/null +++ b/.github/workflows/script/graphql-commit-schema.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +if [ "$1" = 1 ]; then + pnpm api dev +elif [ "$1" = 2 ]; then + sleep 15 && pnpm --filter='@shio/website' gql:introspect +fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e8361a --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# deliverable +.next/ +build/ +dist/ +out/ + +# dependency +node_modules/ +.pnpm-store/ + +# env file +.env* +!.env.example + +# misc +.DS_Store +*.pem + +# storybook +**/.storybook/static/ + +# testing +coverage/ + +# typescript +*.tsbuildinfo + +# vercel +.vercel/ +.turbo/ +.vercel diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..a5a29d9 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +pnpm lint-staged diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..cc8a270 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +auto-install-peers = true +engine-strict = true +save-exact = true diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c8e48ca --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,182 @@ +{ + "commit-message-editor.tokens": [ + { + "label": "Type", + "name": "type", + "type": "enum", + "description": "Type of changes.", + "combobox": true, + "options": [ + { + "label": "feat: โœจ", + "value": "feat: โœจ", + "description": "Implementation of new features." + }, + { + "label": "feat: ๐ŸŽˆ", + "value": "feat: ๐ŸŽˆ", + "description": "Repair of existing features." + }, + { + "label": "feat: โšฐ๏ธ", + "value": "feat: โšฐ๏ธ", + "description": "Deletion of features." + }, + { + "label": "fix: ๐Ÿ›", + "value": "fix: ๐Ÿ›", + "description": "Bug fixes." + }, + { + "label": "fix: ๐Ÿš‘๏ธ", + "value": "fix: ๐Ÿš‘๏ธ", + "description": "Critical bug fixes or major changes." + }, + { + "label": "doc: ๐Ÿ“", + "value": "doc: ๐Ÿ“", + "description": "Documentation changes." + }, + { + "label": "typo: ๐Ÿ–‹๏ธ", + "value": "typo: ๐Ÿ–‹๏ธ", + "description": "Typography changes." + }, + { + "label": "style: ๐Ÿ’„", + "value": "style: ๐Ÿ’„", + "description": "Style changes." + }, + { + "label": "refactor: โ™ป๏ธ", + "value": "refactor: โ™ป๏ธ", + "description": "Code formatting or refactoring." + }, + { + "label": "test: ๐Ÿงช", + "value": "test: ๐Ÿงช", + "description": "Test cases changes." + }, + { + "label": "ci: ๐Ÿฆบ", + "value": "ci: ๐Ÿฆบ", + "description": "CI changes." + }, + { + "label": "build: ๐Ÿ“ฆ๏ธ", + "value": "build: ๐Ÿ“ฆ๏ธ", + "description": "Build system or dependency changes." + }, + { + "label": "container: ๐Ÿณ", + "value": "container: ๐Ÿณ", + "description": "The Dockerfile changes." + }, + { + "label": "container: ๐Ÿ™", + "value": "container: ๐Ÿ™", + "description": "The docker-compose changes." + }, + { + "label": "chore: ๐Ÿ”ง", + "value": "chore: ๐Ÿ”ง", + "description": "Configuration changes." + }, + { + "label": "chore: ๐Ÿ”จ", + "value": "chore: ๐Ÿ”จ", + "description": "Development script changes." + }, + { + "label": "chore: ๐Ÿฑ", + "value": "chore: ๐Ÿฑ", + "description": "Assets changes." + }, + { + "label": "revert: โช๏ธ", + "value": "revert: โช๏ธ", + "description": "Reversion of changes." + }, + { + "label": "wip: ๐Ÿšง", + "value": "wip: ๐Ÿšง", + "description": "Changes that will be squashed." + }, + { + "label": "initial: ๐ŸŽ‰", + "value": "initial: ๐ŸŽ‰", + "description": "The first commit." + } + ] + }, + { + "label": "Scope", + "name": "scope", + "type": "text", + "description": "Scope of changes.", + "prefix": " (", + "suffix": ")" + }, + { + "label": "Short Description", + "name": "description", + "type": "text", + "description": "Commit summary.", + "prefix": " " + }, + { + "label": "Body", + "name": "body", + "type": "text", + "description": "Detailed description of commit.", + "maxLines": 10, + "multiline": true, + "lines": 5 + }, + { + "label": "Footer", + "name": "footer", + "description": "Description of disruptive changes or signature.", + "type": "text", + "multiline": true + } + ], + "commit-message-editor.dynamicTemplate": [ + "{type}{scope}{description}", + "", + "{body}", + "", + "{footer}" + ], + "commit-message-editor.staticTemplate": [ + "label: emoji (scope) short-description", + "", + "body", + "", + "footer" + ], + "commit-message-editor.view.defaultView": "form", + "editor.defaultFormatter": "esbenp.prettier-vscode", + "eslint.format.enable": true, + "eslint.workingDirectories": [ + "apps/api/", + "apps/catalog/", + "apps/website/", + "packages/core/", + "packages/design-token/", + "packages/jest/", + "packages/postcss/", + "packages/stylelint", + "packages/tailwind/", + "packages/type/", + "packages/urql/" + ], + "files.encoding": "utf8", + "files.eol": "\n", + "jest.jestCommandLine": "pnpm all -- test", + "tailwindCSS.classAttributes": ["class", "className"], + "tailwindCSS.experimental.classRegex": [ + ["tv\\(([^)]*)\\)", "[\"']([^\"']*).*?[\"']"], + ["className:.*cn\\(([^)]*)\\)", "[\"']([^\"']*).*?[\"']"] + ] +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d44270d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +# MIT License + +Copyright © 2023 NITIC PBL P8 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..92516fc --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ + + +

+ + + + Locker.ai + +

+ +

+ + + +

+ +

+ + + +

+ +# Locker.ai ๐Ÿ—๏ธ + +Locker.ai is a service that uses a unique AI-driven authentication mechanism to safely report and retrieve lost items. + +## Core Contributors ๐Ÿ› ๏ธ + +| shio | st20089ki | ituki | +| :--------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------: | +| [](https://github.com/dino3616) | [](https://github.com/st20089ki) | [](https://github.com/ituki0426) | +| `#repository-owner` `#designer` `#backend-engineer` | `#frontend-engineer` | `#backend-engineer` `#ml-engineer` | + +## Setup with Docker Compose ๐Ÿ™ + +You can easily launch the development environment of Locker.ai with Docker Compose. +Here is the step-by-step guide. + +### Attention + +- This method cannot build the [Supabase development environment locally](https://supabase.com/docs/guides/cli/local-development). If you wish to do so, please refer to the next section, [Setup locally](#setup-locally-๏ธ). +- You need to install [Docker](https://docs.docker.com/get-docker) and [Docker Compose](https://docs.docker.com/compose/install) before. +- Developers in a Windows environment are encouraged to run Docker on WSL2 instead of directly on Windows due to performance concerns. +- [Optional] You should install project recommended VSCode extensions that specified in [`.devcontainer/devcontainer.json`](./.devcontainer/devcontainer.json#L8C6-L38C8) before. + +### 1. clone git repository + +```bash +git clone "https://github.com/nitic-pbl-p8/lockerai" && cd "./lockerai" +``` + +### 2. launch conatiner + +```bash +docker compose -f "./docker/docker-compose.development.yaml" -p "lockerai" up -d +``` + +### 3. set environment variables + +See `.env.example` or contact the [repository owner](https://github.com/dino3616) for more details. + +### 4. install dependencies + +```bash +pnpm install +``` + +## Setup locally ๐Ÿ–ฅ๏ธ + +If you need to build the [Supabase development environment locally](https://supabase.com/docs/guides/cli/local-development), please follow the steps below. + +### Attention + +- You need to install [Docker](https://docs.docker.com/get-docker) and [Docker Compose](https://docs.docker.com/compose/install), and [Volta](https://docs.volta.sh/guide/getting-started) (optional) before. +- You need to install [Node.js](https://nodejs.org/en/download) and [pnpm](https://pnpm.io/installation) that specified in [`package.json`](./package.json#L7C2-L15C5) before. (With Volta, you can easily install a specified version of the tool. Recommendation.) +- [Optional] You should install project recommended VSCode extensions that specified in [`.devcontainer/devcontainer.json`](./.devcontainer/devcontainer.json#L8C6-L38C8) before. + +### 1. clone git repository + +```bash +git clone "https://github.com/nitic-pbl-p8/lockerai" && cd "./lockerai" +``` + +### 2. set environment variables + +See `.env.example` or contact the [repository owner](https://github.com/dino3616) for more details. + +### 3. install dependencies + +```bash +pnpm install +``` + +### 4. setup supabase + +See [Supabase official documentation](https://supabase.com/docs/guides/cli/local-development) for more details. + +## Project Useful Links ๐Ÿ“š + +Here are some useful links for this project. + +## Design + +- [Figma](https://www.figma.com/file/xNKAhniAfPPTsL987xRCVe/website?type=design&node-id=20%3A35&mode=design&t=oAlQP6Jqqk0ZcqOy-1) + +## Component Catalog + +- [Storybook](https://main--653402bb608cd8eb750c3867.chromatic.com) +- [Chromatic](https://www.chromatic.com/library?appId=653402bb608cd8eb750c3867&branch=main) diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 0000000..a0b9d40 --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,5 @@ +APOLLO_KEY="service::" +APOLLO_GRAPH_REF="@" +APOLLO_SCHEMA_REPORTING="true" +DATABASE_SSL_CERT="" +DATABASE_URL="postgresql://:@:/" diff --git a/apps/api/.eslintignore b/apps/api/.eslintignore new file mode 100644 index 0000000..9c054e2 --- /dev/null +++ b/apps/api/.eslintignore @@ -0,0 +1,4 @@ +coverage/ +dist/ +generated/ +node_modules/ diff --git a/apps/api/.eslintrc.cjs b/apps/api/.eslintrc.cjs new file mode 100644 index 0000000..9720fd0 --- /dev/null +++ b/apps/api/.eslintrc.cjs @@ -0,0 +1,4 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: ['lockerai-node'], +}; diff --git a/apps/api/.gitignore b/apps/api/.gitignore new file mode 100644 index 0000000..4a545a7 --- /dev/null +++ b/apps/api/.gitignore @@ -0,0 +1,8 @@ +# graphql schema +schema.gql + +# auto generated folder +generated + +# certificates +*.crt diff --git a/apps/api/.lintstagedrc.cjs b/apps/api/.lintstagedrc.cjs new file mode 100644 index 0000000..ba79164 --- /dev/null +++ b/apps/api/.lintstagedrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + '**/*.{js,ts}': (/** @type {string[]} */ filenames) => `pnpm eslint --fix ${filenames.join(' --fix ')}`, + '**/*.{js,ts,json}': (/** @type {string[]} */ filenames) => `pnpm prettier --check ${filenames.join(' --check ')}`, +}; diff --git a/apps/api/.prettierignore b/apps/api/.prettierignore new file mode 100644 index 0000000..9c054e2 --- /dev/null +++ b/apps/api/.prettierignore @@ -0,0 +1,4 @@ +coverage/ +dist/ +generated/ +node_modules/ diff --git a/apps/api/.prettierrc.cjs b/apps/api/.prettierrc.cjs new file mode 100644 index 0000000..edc830e --- /dev/null +++ b/apps/api/.prettierrc.cjs @@ -0,0 +1,4 @@ +/** @type {import('prettier').Options} */ +module.exports = { + ...require('@lockerai/prettier'), +}; diff --git a/apps/api/drizzle.config.ts b/apps/api/drizzle.config.ts new file mode 100644 index 0000000..680762c --- /dev/null +++ b/apps/api/drizzle.config.ts @@ -0,0 +1,34 @@ +import dotenv from 'dotenv'; +import type { Config } from 'drizzle-kit'; +import { match } from 'ts-pattern'; + +const envFilePath = match(process.env['NODE_ENV']) + .with('development', () => ['.env', '.env.development']) + .with('production', () => ['.env', '.env.production']) + .with('test', () => ['.env', '.env.test']) + .otherwise(() => ['.env', '.env.development']); + +envFilePath.forEach((path) => dotenv.config({ path })); + +if (!process.env['DATABASE_URL']) { + throw new Error('DATABASE_URL is not defined.'); +} + +const drizzleConfig: Config = { + schema: './src/infra/drizzle/schema/**/*.ts', + out: './src/infra/drizzle/migration', + breakpoints: true, + strict: true, + driver: 'pg', + dbCredentials: + process.env['NODE_ENV'] === 'production' + ? { + connectionString: `${process.env['DATABASE_URL']}?sslmode=verify-full&sslrootcert=prod-ca-2021.crt`, + ssl: true, + } + : { + connectionString: process.env['DATABASE_URL'], + }, +}; + +export default drizzleConfig; diff --git a/apps/api/jest.config.cjs b/apps/api/jest.config.cjs new file mode 100644 index 0000000..354e666 --- /dev/null +++ b/apps/api/jest.config.cjs @@ -0,0 +1,6 @@ +const config = require('@lockerai/jest/jest.node.cjs'); + +/** @type {import('jest').Config} */ +module.exports = { + ...config, +}; diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..b053a79 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,67 @@ +{ + "name": "@lockerai/api", + "version": "0.1.0", + "repository": "https://github.com/cardo-lockerai/lockerai.git", + "author": "dino3616 <85730998+dino3616@users.noreply.github.com>", + "license": "MIT", + "private": true, + "type": "commonjs", + "module": "nodenext", + "scripts": { + "prebuild": "rimraf dist", + "build": "nest build", + "dev": "cross-env NODE_ENV=development nest start --watch", + "start": "cross-env NODE_ENV=production nest start", + "start:dev": "cross-env NODE_ENV=development nest start", + "prod": "cross-env NODE_ENV=production node dist/main", + "prod:dev": "cross-env NODE_ENV=development node dist/main", + "lint": "eslint './**/{*.*js,*.*ts}'", + "lint:fix": "eslint './**/{*.*js,*.*ts}' --fix", + "fmt": "prettier --check .", + "fmt:fix": "prettier --write .", + "test": "cross-env NODE_ENV=test jest --silent=false --verbose false --passWithNoTests", + "db:generate": "drizzle-kit generate:pg --config ./drizzle.config.ts", + "db:push:dev": "cross-env NODE_ENV=development drizzle-kit push:pg --config ./drizzle.config.ts", + "db:push:prod": "cross-env NODE_ENV=production drizzle-kit push:pg --config ./drizzle.config.ts", + "db:push:test": "cross-env NODE_ENV=test drizzle-kit push:pg --config ./drizzle.config.ts", + "db:studio:dev": "cross-env NODE_ENV=development drizzle-kit studio --config ./drizzle.config.ts", + "db:studio:prod": "cross-env NODE_ENV=production drizzle-kit studio --config ./drizzle.config.ts", + "apollo:rover": "rover graph publish lockerai@current --schema ./schema.gql" + }, + "dependencies": { + "@apollo/server": "4.9.4", + "@nestjs/apollo": "12.0.9", + "@nestjs/cli": "10.1.18", + "@nestjs/common": "10.2.7", + "@nestjs/config": "3.1.1", + "@nestjs/core": "10.2.7", + "@nestjs/graphql": "12.0.9", + "@nestjs/platform-express": "10.2.7", + "@nestjs/testing": "10.2.7", + "class-transformer": "0.5.1", + "class-validator": "0.14.0", + "dataloader": "2.2.2", + "dotenv": "16.3.1", + "drizzle-orm": "0.28.6", + "graphql-subscriptions": "2.0.0", + "graphql-validation-complexity": "0.4.2", + "postgres": "3.4.0", + "ts-pattern": "5.0.5", + "uuid": "9.0.1" + }, + "devDependencies": { + "@apollo/rover": "0.20.0", + "@lockerai/jest": "workspace:*", + "@lockerai/postcss": "workspace:*", + "@lockerai/prettier": "workspace:*", + "@lockerai/stylelint": "workspace:*", + "@lockerai/tsconfig": "workspace:*", + "@lockerai/type": "workspace:*", + "@nestjs/schematics": "10.0.2", + "@types/graphql-validation-complexity": "0.4.3", + "@types/pg": "8.10.7", + "drizzle-kit": "0.19.13", + "eslint-config-lockerai-node": "workspace:*", + "pg": "8.11.3" + } +} diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts new file mode 100644 index 0000000..8bf4a95 --- /dev/null +++ b/apps/api/src/app/app.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { CacheModule } from '#api/common/service/cache/cache.module'; +import { EnvModule } from '#api/common/service/env/env.module'; +import { PubSubModule } from '#api/common/service/pubsub/pubsub.module'; +import { GraphQLConfigModule } from '#api/config/graphql/graphql-config.module'; +import { DrizzleModule } from '#api/infra/drizzle/drizzle.module'; +import { Modules } from '#api/module'; + +@Module({ + imports: [EnvModule, GraphQLConfigModule, DrizzleModule, CacheModule, PubSubModule, ...Modules], +}) +export class AppModule {} diff --git a/apps/api/src/common/constant/injection-token.ts b/apps/api/src/common/constant/injection-token.ts new file mode 100644 index 0000000..e25afcb --- /dev/null +++ b/apps/api/src/common/constant/injection-token.ts @@ -0,0 +1,5 @@ +// eslint-disable-next-line no-shadow +export enum InjectionToken { + USER_REPOSITORY = 'USER_REPOSITORY', + USER_USE_CASE = 'USER_USE_CASE', +} diff --git a/apps/api/src/common/service/cache/base.dataloader.ts b/apps/api/src/common/service/cache/base.dataloader.ts new file mode 100644 index 0000000..0e26817 --- /dev/null +++ b/apps/api/src/common/service/cache/base.dataloader.ts @@ -0,0 +1,27 @@ +import DataLoader from 'dataloader'; + +export abstract class BaseDataLoader extends Object { + protected dataloader: DataLoader = new DataLoader(this.batchLoad.bind(this)); + + public clear(key: K): DataLoader { + return this.dataloader.clear(key); + } + + public clearAll(): DataLoader { + return this.dataloader.clearAll(); + } + + public async load(key: K): Promise { + return this.dataloader.load(key); + } + + public async loadMany(keys: K[]): Promise<(V | Error)[]> { + return this.dataloader.loadMany(keys); + } + + public prime(key: K, value: V): DataLoader { + return this.dataloader.prime(key, value); + } + + protected abstract batchLoad(keys: readonly K[]): Promise<(V | Error)[]>; +} diff --git a/apps/api/src/common/service/cache/cache.module.ts b/apps/api/src/common/service/cache/cache.module.ts new file mode 100644 index 0000000..2f25fae --- /dev/null +++ b/apps/api/src/common/service/cache/cache.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { DataLoaderCacheService } from './data-loader-cache.service'; + +@Global() +@Module({ + providers: [DataLoaderCacheService], + exports: [DataLoaderCacheService], +}) +export class CacheModule {} diff --git a/apps/api/src/common/service/cache/data-loader-cache.service.ts b/apps/api/src/common/service/cache/data-loader-cache.service.ts new file mode 100644 index 0000000..32c0141 --- /dev/null +++ b/apps/api/src/common/service/cache/data-loader-cache.service.ts @@ -0,0 +1,22 @@ +import { Injectable, Logger } from '@nestjs/common'; +import type DataLoader from 'dataloader'; + +@Injectable() +// eslint-disable-next-line no-use-before-define +export class DataLoaderCacheService, K, C = K> { + private readonly logger = new Logger(DataLoaderCacheService.name); + + constructor() { + this.logger.debug(`${DataLoaderCacheService.name} constructed`); + } + + prime(dataLoader: DataLoader, obj: T) { + dataLoader.prime(obj.id, obj); + } + + primeMany(dataLoader: DataLoader, objs: T[]) { + objs.forEach((obj) => { + dataLoader.prime(obj.id, obj); + }); + } +} diff --git a/apps/api/src/common/service/env/env.module.ts b/apps/api/src/common/service/env/env.module.ts new file mode 100644 index 0000000..8c3b530 --- /dev/null +++ b/apps/api/src/common/service/env/env.module.ts @@ -0,0 +1,25 @@ +import { Global, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { match } from 'ts-pattern'; +import { EnvService } from './env.service'; +import { validate } from './validate'; + +const envFilePath = match(process.env['NODE_ENV']) + .with('development', () => ['.env', '.env.development']) + .with('production', () => ['.env', '.env.production']) + .with('test', () => ['.env', '.env.test']) + .otherwise(() => ['.env', '.env.development']); + +@Global() +@Module({ + imports: [ + ConfigModule.forRoot({ + envFilePath, + isGlobal: true, + validate, + }), + ], + providers: [EnvService], + exports: [EnvService], +}) +export class EnvModule {} diff --git a/apps/api/src/common/service/env/env.service.ts b/apps/api/src/common/service/env/env.service.ts new file mode 100644 index 0000000..13e85fa --- /dev/null +++ b/apps/api/src/common/service/env/env.service.ts @@ -0,0 +1,46 @@ +import type { ApolloConfigInput } from '@apollo/server'; +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class EnvService { + private readonly logger = new Logger(EnvService.name); + + constructor(@Inject(ConfigService) private readonly configService: ConfigService) { + this.logger.debug(`${EnvService.name} constructed`); + this.logger.log(`NODE_ENV: ${this.NodeEnv}`); + } + + get NodeEnv(): 'development' | 'production' | 'test' { + const nodeEnv = this.configService.get<'development' | 'production' | 'test'>('NODE_ENV', 'development'); + + return nodeEnv; + } + + get Port(): number { + const port = this.configService.get('PORT', 4000); + + return port; + } + + get DatabaseUrl(): string { + const databaseUrl = this.configService.getOrThrow('DATABASE_URL'); + + return databaseUrl; + } + + get DatabaseSslCert(): string { + const databaseSslCert = this.configService.getOrThrow('DATABASE_SSL_CERT'); + + return databaseSslCert; + } + + get ApolloStudioConfig(): ApolloConfigInput { + const apolloConfigInput: ApolloConfigInput = { + key: this.configService.getOrThrow('APOLLO_KEY'), + graphRef: this.configService.getOrThrow('APOLLO_GRAPH_REF'), + }; + + return apolloConfigInput; + } +} diff --git a/apps/api/src/common/service/env/validate.ts b/apps/api/src/common/service/env/validate.ts new file mode 100644 index 0000000..b7c3663 --- /dev/null +++ b/apps/api/src/common/service/env/validate.ts @@ -0,0 +1,36 @@ +import { Transform, plainToClass } from 'class-transformer'; +import { IsEnum, IsNumber, IsUrl, validateSync } from 'class-validator'; + +export class EnvValidator { + @IsEnum(['development', 'production', 'test']) + NODE_ENV!: 'development' | 'production' | 'test'; + + @Transform(({ value }) => parseInt(value, 10)) + @IsNumber() + PORT = 4000; + + @IsUrl({ protocols: ['postgresql'], require_tld: process.env['NODE_ENV'] === 'production' }) + DATABASE_URL!: string; + + DATABASE_SSL_CERT!: string; + + APOLLO_KEY!: string; + + APOLLO_GRAPH_REF!: string; +} + +export const validate = (config: Record) => { + const validatedConfig = plainToClass(EnvValidator, config, { + enableImplicitConversion: true, + }); + + const errors = validateSync(validatedConfig, { + skipMissingProperties: false, + }); + + if (errors.length > 0) { + throw new Error(errors.toString()); + } + + return validatedConfig; +}; diff --git a/apps/api/src/common/service/pubsub/pubsub.module.ts b/apps/api/src/common/service/pubsub/pubsub.module.ts new file mode 100644 index 0000000..3cc0f33 --- /dev/null +++ b/apps/api/src/common/service/pubsub/pubsub.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PubSubService } from './pubsub.service'; + +@Global() +@Module({ + providers: [PubSubService], + exports: [PubSubService], +}) +export class PubSubModule {} diff --git a/apps/api/src/common/service/pubsub/pubsub.service.ts b/apps/api/src/common/service/pubsub/pubsub.service.ts new file mode 100644 index 0000000..0e250bd --- /dev/null +++ b/apps/api/src/common/service/pubsub/pubsub.service.ts @@ -0,0 +1,13 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PubSub } from 'graphql-subscriptions'; + +@Injectable() +export class PubSubService extends PubSub { + private readonly logger = new Logger(PubSubService.name); + + constructor() { + super(); + + this.logger.debug(`${PubSubService.name} constructed`); + } +} diff --git a/apps/api/src/config/graphql/graphql-config.module.ts b/apps/api/src/config/graphql/graphql-config.module.ts new file mode 100644 index 0000000..6afbb8c --- /dev/null +++ b/apps/api/src/config/graphql/graphql-config.module.ts @@ -0,0 +1,44 @@ +import { join } from 'path'; +import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'; +import { ApolloDriver, type ApolloDriverConfig } from '@nestjs/apollo'; +import { GraphQLModule } from '@nestjs/graphql'; +import { createComplexityLimitRule } from 'graphql-validation-complexity'; +import { match } from 'ts-pattern'; +import { EnvService } from '#api/common/service/env/env.service'; + +const baseConfig: ApolloDriverConfig = { + autoSchemaFile: join(process.cwd(), './schema.gql'), + cache: 'bounded', + context: undefined, + introspection: true, + path: '/graphql', + playground: false, + plugins: [ApolloServerPluginLandingPageLocalDefault()], + sortSchema: true, + subscriptions: { + 'graphql-ws': true, + }, + validationRules: [createComplexityLimitRule(5000)], +}; + +const createDevelopmentConfig = (): ApolloDriverConfig => baseConfig; + +const createProductionConfig = (envService: EnvService): ApolloDriverConfig => ({ + ...baseConfig, + apollo: envService.ApolloStudioConfig, +}); + +const createTestConfig = (): ApolloDriverConfig => baseConfig; + +const gqlFactory = (envService: EnvService): ApolloDriverConfig => + match(envService.NodeEnv) + .with('development', () => createDevelopmentConfig()) + .with('production', () => createProductionConfig(envService)) + .with('test', () => createTestConfig()) + .otherwise(() => createDevelopmentConfig()); + +export const GraphQLConfigModule = GraphQLModule.forRootAsync({ + driver: ApolloDriver, + useFactory: gqlFactory, + inject: [EnvService], +}); diff --git a/apps/api/src/infra/drizzle/drizzle.module.ts b/apps/api/src/infra/drizzle/drizzle.module.ts new file mode 100644 index 0000000..890fca9 --- /dev/null +++ b/apps/api/src/infra/drizzle/drizzle.module.ts @@ -0,0 +1,32 @@ +import { Global, Module } from '@nestjs/common'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import { EnvService } from '#api/common/service/env/env.service'; +import * as schema from './schema'; + +export const PG_CONNECTION = 'PG_CONNECTION'; + +export type PgConnection = ReturnType>; + +@Global() +@Module({ + providers: [ + { + provide: PG_CONNECTION, + inject: [EnvService], + useFactory: (envService: EnvService): PgConnection => { + const pool = new Pool({ + connectionString: envService.DatabaseUrl, + ssl: envService.NodeEnv === 'production' && { + ca: envService.DatabaseSslCert, + cert: envService.DatabaseSslCert, + }, + }); + + return drizzle(pool, { schema, logger: true }); + }, + }, + ], + exports: [PG_CONNECTION], +}) +export class DrizzleModule {} diff --git a/apps/api/src/infra/drizzle/schema/constant/relation-key.ts b/apps/api/src/infra/drizzle/schema/constant/relation-key.ts new file mode 100644 index 0000000..11ab0fb --- /dev/null +++ b/apps/api/src/infra/drizzle/schema/constant/relation-key.ts @@ -0,0 +1,6 @@ +export const relationKey = { + 'drawer-and-lost-item': 'drawer-and-lost-item', + 'locker-and-drawers': 'locker-and-drawers', + 'owner-and-retrieved-lost-items': 'owner-and-retrieved-lost-items', + 'reporter-and-reported-lost-items': 'reporter-and-reported-lost-items', +} as const satisfies Record; diff --git a/apps/api/src/infra/drizzle/schema/drawers.ts b/apps/api/src/infra/drizzle/schema/drawers.ts new file mode 100644 index 0000000..1e6df4a --- /dev/null +++ b/apps/api/src/infra/drizzle/schema/drawers.ts @@ -0,0 +1,18 @@ +import { relations } from 'drizzle-orm'; +import { pgTable, serial, timestamp, uuid } from 'drizzle-orm/pg-core'; +import { relationKey } from './constant/relation-key'; +import { lockers } from './lockers'; +import { lostItems } from './lost-items'; + +export const drawers = pgTable('drawers', { + id: serial('id').primaryKey(), + lockerId: uuid('locker_id') + .references(() => lockers.id) + .notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), +}); + +export const drawersRelations = relations(drawers, ({ one }) => ({ + locker: one(lockers, { fields: [drawers.lockerId], references: [lockers.id], relationName: relationKey['locker-and-drawers'] }), + lostItem: one(lostItems, { fields: [drawers.id], references: [lostItems.drawerId], relationName: relationKey['drawer-and-lost-item'] }), +})); diff --git a/apps/api/src/infra/drizzle/schema/enum/lost-and-found-state.ts b/apps/api/src/infra/drizzle/schema/enum/lost-and-found-state.ts new file mode 100644 index 0000000..88deaab --- /dev/null +++ b/apps/api/src/infra/drizzle/schema/enum/lost-and-found-state.ts @@ -0,0 +1,3 @@ +import { pgEnum } from 'drizzle-orm/pg-core'; + +export const lostAndFoundStateEnum = pgEnum('lost_and_found_state', ['NONE', 'DELIVERING', 'RETRIEVING']); diff --git a/apps/api/src/infra/drizzle/schema/index.ts b/apps/api/src/infra/drizzle/schema/index.ts new file mode 100644 index 0000000..c0f4f3f --- /dev/null +++ b/apps/api/src/infra/drizzle/schema/index.ts @@ -0,0 +1,5 @@ +export { lostAndFoundStateEnum } from './enum/lost-and-found-state'; +export { drawers, drawersRelations } from './drawers'; +export { lockers, lockersRelations } from './lockers'; +export { lostItems, lostItemsRelations } from './lost-items'; +export { users, usersRelations } from './users'; diff --git a/apps/api/src/infra/drizzle/schema/lockers.ts b/apps/api/src/infra/drizzle/schema/lockers.ts new file mode 100644 index 0000000..b60e859 --- /dev/null +++ b/apps/api/src/infra/drizzle/schema/lockers.ts @@ -0,0 +1,14 @@ +import { relations } from 'drizzle-orm'; +import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; +import { relationKey } from './constant/relation-key'; +import { drawers } from './drawers'; + +export const lockers = pgTable('lockers', { + id: uuid('id').primaryKey().defaultRandom(), + name: varchar('name', { length: 32 }).unique().notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), +}); + +export const lockersRelations = relations(lockers, ({ many }) => ({ + drawers: many(drawers, { relationName: relationKey['locker-and-drawers'] }), +})); diff --git a/apps/api/src/infra/drizzle/schema/lost-items.ts b/apps/api/src/infra/drizzle/schema/lost-items.ts new file mode 100644 index 0000000..a4ae414 --- /dev/null +++ b/apps/api/src/infra/drizzle/schema/lost-items.ts @@ -0,0 +1,26 @@ +import { relations } from 'drizzle-orm'; +import { pgTable, serial, text, timestamp, uuid } from 'drizzle-orm/pg-core'; +import { relationKey } from './constant/relation-key'; +import { drawers } from './drawers'; +import { users } from './users'; + +export const lostItems = pgTable('lost_items', { + id: uuid('id').primaryKey().defaultRandom(), + // TODO: Determine the best json type according to the Google Vision API response. + // features: jsonb('features').notNull(), + imageUrls: text('image_urls').array().notNull(), + drawerId: serial('drawer_id').references(() => drawers.id), + reporterId: uuid('reporter_id') + .references(() => users.id) + .notNull(), + ownerId: uuid('owner_id').references(() => users.id), + reportedAt: timestamp('reported_at').notNull().defaultNow(), + deliveredAt: timestamp('delivered_at'), + retrievedAt: timestamp('retrieved_at'), +}); + +export const lostItemsRelations = relations(lostItems, ({ one }) => ({ + drawer: one(drawers, { fields: [lostItems.drawerId], references: [drawers.id], relationName: relationKey['drawer-and-lost-item'] }), + reporter: one(users, { fields: [lostItems.reporterId], references: [users.id], relationName: relationKey['reporter-and-reported-lost-items'] }), + owner: one(users, { fields: [lostItems.ownerId], references: [users.id], relationName: relationKey['owner-and-retrieved-lost-items'] }), +})); diff --git a/apps/api/src/infra/drizzle/schema/users.ts b/apps/api/src/infra/drizzle/schema/users.ts new file mode 100644 index 0000000..6285bf0 --- /dev/null +++ b/apps/api/src/infra/drizzle/schema/users.ts @@ -0,0 +1,20 @@ +import { relations } from 'drizzle-orm'; +import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'; +import { relationKey } from './constant/relation-key'; +import { lostAndFoundStateEnum } from './enum/lost-and-found-state'; +import { lostItems } from './lost-items'; + +export const users = pgTable('users', { + id: uuid('id').primaryKey().defaultRandom(), + // TODO: Change to varchar as soon as the length of the string is known. + authId: text('auth_id').unique().notNull(), + // TODO: Change to varchar as soon as the length of the string is known. + fingerprintId: text('fingerprint_id').unique(), + lostAndFoundState: lostAndFoundStateEnum('lost_and_found_state').notNull().default('NONE'), + createdAt: timestamp('created_at').notNull().defaultNow(), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + reportedLostItems: many(lostItems, { relationName: relationKey['reporter-and-reported-lost-items'] }), + retrievedLostItems: many(lostItems, { relationName: relationKey['owner-and-retrieved-lost-items'] }), +})); diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts new file mode 100644 index 0000000..d78d8ff --- /dev/null +++ b/apps/api/src/main.ts @@ -0,0 +1,20 @@ +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app/app.module'; +import { EnvService } from './common/service/env/env.service'; + +const bootstrap = async () => { + const logger = new Logger(bootstrap.name); + + const app = await NestFactory.create(AppModule, { + cors: true, + }); + + const port = app.get(EnvService).Port; + + logger.log(`Locker.ai API is running on port ${port} ๐Ÿš€`); + + await app.listen(port); +}; + +bootstrap(); diff --git a/apps/api/src/module/index.ts b/apps/api/src/module/index.ts new file mode 100644 index 0000000..e426ed4 --- /dev/null +++ b/apps/api/src/module/index.ts @@ -0,0 +1,3 @@ +import { UserModule } from './user/user.module'; + +export const Modules = [UserModule]; diff --git a/apps/api/src/module/user/controller/dto/enum/user-lost-and-found-state.enum.ts b/apps/api/src/module/user/controller/dto/enum/user-lost-and-found-state.enum.ts new file mode 100644 index 0000000..020c80d --- /dev/null +++ b/apps/api/src/module/user/controller/dto/enum/user-lost-and-found-state.enum.ts @@ -0,0 +1,10 @@ +import { registerEnumType } from '@nestjs/graphql'; + +// eslint-disable-next-line no-shadow +export enum UserLostAndFoundStateEnum { + NONE = 'NONE', + DELIVERING = 'DELIVERING', + RETRIEVING = 'RETRIEVING', +} + +registerEnumType(UserLostAndFoundStateEnum, { name: 'UserLostAndFoundState' }); diff --git a/apps/api/src/module/user/controller/dto/input/user-create.input.ts b/apps/api/src/module/user/controller/dto/input/user-create.input.ts new file mode 100644 index 0000000..7d61f93 --- /dev/null +++ b/apps/api/src/module/user/controller/dto/input/user-create.input.ts @@ -0,0 +1,8 @@ +import { Field, InputType } from '@nestjs/graphql'; +import type { User } from '#api/module/user/domain/user.model'; + +@InputType() +export class UserCreateInput implements Omit { + @Field(() => String, { nullable: false }) + authId!: string; +} diff --git a/apps/api/src/module/user/controller/dto/input/user-where-unique.input.ts b/apps/api/src/module/user/controller/dto/input/user-where-unique.input.ts new file mode 100644 index 0000000..41255a0 --- /dev/null +++ b/apps/api/src/module/user/controller/dto/input/user-where-unique.input.ts @@ -0,0 +1,10 @@ +import { Field, ID, InputType } from '@nestjs/graphql'; +import { IsUUID } from 'class-validator'; +import type { User } from '#api/module/user/domain/user.model'; + +@InputType() +export class UserWhereUniqueInput implements Record<'userId', User['id']> { + @Field(() => ID, { nullable: false }) + @IsUUID() + userId!: string; +} diff --git a/apps/api/src/module/user/controller/dto/object/user.object.ts b/apps/api/src/module/user/controller/dto/object/user.object.ts new file mode 100644 index 0000000..ae21cbc --- /dev/null +++ b/apps/api/src/module/user/controller/dto/object/user.object.ts @@ -0,0 +1,23 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { IsUUID } from 'class-validator'; +import { UserLostAndFoundStateEnum } from '#api/module/user/controller/dto/enum/user-lost-and-found-state.enum'; +import { User } from '#api/module/user/domain/user.model'; + +@ObjectType(User.name) +export class UserObject implements User { + @Field(() => ID, { nullable: false }) + @IsUUID() + id!: string; + + @Field(() => ID, { nullable: false }) + authId!: string; + + @Field(() => ID, { nullable: true }) + fingerprintId!: string | null; + + @Field(() => UserLostAndFoundStateEnum, { nullable: false }) + lostAndFoundState!: UserLostAndFoundStateEnum; + + @Field(() => Date, { nullable: false }) + createdAt!: Date; +} diff --git a/apps/api/src/module/user/controller/user-mutation.resolver.ts b/apps/api/src/module/user/controller/user-mutation.resolver.ts new file mode 100644 index 0000000..9fb9567 --- /dev/null +++ b/apps/api/src/module/user/controller/user-mutation.resolver.ts @@ -0,0 +1,31 @@ +import { Inject, Logger, ValidationPipe } from '@nestjs/common'; +import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { InjectionToken } from '#api/common/constant/injection-token'; +import type { User } from '#api/module/user/domain/user.model'; +// TODO: Once this issue is resolved, modify to use `import type` syntax. +// https://github.com/typescript-eslint/typescript-eslint/issues/5468 +import { type UserUseCaseInterface } from '#api/module/user/use-case/user.use-case'; +import { UserCreateInput } from './dto/input/user-create.input'; +import { UserObject } from './dto/object/user.object'; + +@Resolver() +export class UserMutation { + private readonly logger = new Logger(UserMutation.name); + + constructor( + @Inject(InjectionToken.USER_USE_CASE) + private readonly userUseCase: UserUseCaseInterface, + ) {} + + @Mutation(() => UserObject) + async createUser( + @Args('user', { type: () => UserCreateInput }, ValidationPipe) + user: UserCreateInput, + ): Promise { + this.logger.log(`${this.createUser.name} called`); + + const createdUser = await this.userUseCase.createUser(user); + + return createdUser; + } +} diff --git a/apps/api/src/module/user/controller/user-query.resolver.ts b/apps/api/src/module/user/controller/user-query.resolver.ts new file mode 100644 index 0000000..11bb990 --- /dev/null +++ b/apps/api/src/module/user/controller/user-query.resolver.ts @@ -0,0 +1,31 @@ +import { Inject, Logger, ValidationPipe } from '@nestjs/common'; +import { Args, Query, Resolver } from '@nestjs/graphql'; +import { InjectionToken } from '#api/common/constant/injection-token'; +import type { User } from '#api/module/user/domain/user.model'; +// TODO: Once this issue is resolved, modify to use `import type` syntax. +// https://github.com/typescript-eslint/typescript-eslint/issues/5468 +import { type UserUseCaseInterface } from '#api/module/user/use-case/user.use-case'; +import { UserWhereUniqueInput } from './dto/input/user-where-unique.input'; +import { UserObject } from './dto/object/user.object'; + +@Resolver() +export class UserQuery { + private readonly logger = new Logger(UserQuery.name); + + constructor( + @Inject(InjectionToken.USER_USE_CASE) + private readonly userUseCase: UserUseCaseInterface, + ) {} + + @Query(() => UserObject, { nullable: true }) + async findUser( + @Args('where', { type: () => UserWhereUniqueInput }, ValidationPipe) + where: UserWhereUniqueInput, + ): Promise { + this.logger.log(`${this.findUser.name} called`); + + const foundUser = await this.userUseCase.findUser(where.userId); + + return foundUser; + } +} diff --git a/apps/api/src/module/user/domain/user.model.ts b/apps/api/src/module/user/domain/user.model.ts new file mode 100644 index 0000000..400fcfc --- /dev/null +++ b/apps/api/src/module/user/domain/user.model.ts @@ -0,0 +1,21 @@ +type UserLostAndFoundState = 'NONE' | 'DELIVERING' | 'RETRIEVING'; + +export class User { + readonly id: string; + + readonly authId: string; + + readonly fingerprintId: string | null; + + readonly lostAndFoundState: UserLostAndFoundState; + + readonly createdAt: Date; + + constructor({ id, authId, fingerprintId, lostAndFoundState, createdAt }: Omit) { + this.id = id; + this.authId = authId; + this.fingerprintId = fingerprintId; + this.lostAndFoundState = lostAndFoundState; + this.createdAt = createdAt; + } +} diff --git a/apps/api/src/module/user/repository/impl/user.repository.ts b/apps/api/src/module/user/repository/impl/user.repository.ts new file mode 100644 index 0000000..4f8a195 --- /dev/null +++ b/apps/api/src/module/user/repository/impl/user.repository.ts @@ -0,0 +1,29 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { eq } from 'drizzle-orm'; +import { PG_CONNECTION, type PgConnection } from '#api/infra/drizzle/drizzle.module'; +import { users } from '#api/infra/drizzle/schema'; +import { User } from '#api/module/user/domain/user.model'; +import type { UserRepositoryInterface } from '#api/module/user/repository/user.repository'; + +@Injectable() +export class UserRepository implements UserRepositoryInterface { + constructor(@Inject(PG_CONNECTION) private readonly conn: PgConnection) {} + + async find(userId: Parameters[0]): Promise { + const foundUser = (await this.conn.select().from(users).where(eq(users.id, userId))).at(0); + if (foundUser === undefined) { + return null; + } + + return new User(foundUser); + } + + async create(user: Parameters[0]): Promise { + const createdUser = (await this.conn.insert(users).values(user).returning()).at(0); + if (createdUser === undefined) { + throw new Error('Failed to create user'); + } + + return new User(createdUser); + } +} diff --git a/apps/api/src/module/user/repository/user.repository.ts b/apps/api/src/module/user/repository/user.repository.ts new file mode 100644 index 0000000..462e5c5 --- /dev/null +++ b/apps/api/src/module/user/repository/user.repository.ts @@ -0,0 +1,6 @@ +import type { User } from '#api/module/user/domain/user.model'; + +export interface UserRepositoryInterface { + find(userId: User['id']): Promise; + create(user: Omit): Promise; +} diff --git a/apps/api/src/module/user/use-case/impl/user.use-case.ts b/apps/api/src/module/user/use-case/impl/user.use-case.ts new file mode 100644 index 0000000..51f532f --- /dev/null +++ b/apps/api/src/module/user/use-case/impl/user.use-case.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { InjectionToken } from '#api/common/constant/injection-token'; +import type { User } from '#api/module/user/domain/user.model'; +// TODO: Once this issue is resolved, modify to use `import type` syntax. +// https://github.com/typescript-eslint/typescript-eslint/issues/5468 +import { type UserRepositoryInterface } from '#api/module/user/repository/user.repository'; +import type { UserUseCaseInterface } from '#api/module/user/use-case/user.use-case'; + +@Injectable() +export class UserUseCase implements UserUseCaseInterface { + constructor( + @Inject(InjectionToken.USER_REPOSITORY) + private readonly userRepository: UserRepositoryInterface, + ) {} + + async findUser(userId: Parameters[0]): Promise { + const foundUser = await this.userRepository.find(userId); + + return foundUser; + } + + async createUser(user: Parameters[0]): Promise { + const createdUser = await this.userRepository.create(user); + + return createdUser; + } +} diff --git a/apps/api/src/module/user/use-case/user.use-case.ts b/apps/api/src/module/user/use-case/user.use-case.ts new file mode 100644 index 0000000..12a0d22 --- /dev/null +++ b/apps/api/src/module/user/use-case/user.use-case.ts @@ -0,0 +1,6 @@ +import type { User } from '#api/module/user/domain/user.model'; + +export interface UserUseCaseInterface { + findUser(userId: User['id']): Promise; + createUser(user: Omit): Promise; +} diff --git a/apps/api/src/module/user/user.module.ts b/apps/api/src/module/user/user.module.ts new file mode 100644 index 0000000..968285b --- /dev/null +++ b/apps/api/src/module/user/user.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { InjectionToken } from '#api/common/constant/injection-token'; +import { UserMutation } from './controller/user-mutation.resolver'; +import { UserQuery } from './controller/user-query.resolver'; +import { UserRepository } from './repository/impl/user.repository'; +import { UserUseCase } from './use-case/impl/user.use-case'; + +@Module({ + providers: [ + { provide: InjectionToken.USER_REPOSITORY, useClass: UserRepository }, + { provide: InjectionToken.USER_USE_CASE, useClass: UserUseCase }, + UserQuery, + UserMutation, + ], + exports: [{ provide: InjectionToken.USER_REPOSITORY, useClass: UserRepository }], +}) +export class UserModule {} diff --git a/apps/api/tsconfig.build.json b/apps/api/tsconfig.build.json new file mode 100644 index 0000000..d14e690 --- /dev/null +++ b/apps/api/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["./drizzle.config.ts", "./*.*js", "**/test/**/*", "**/*.spec.ts"] +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..6ea1f3f --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@lockerai/tsconfig/tsconfig.node.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/", + "paths": { + "#api/*": ["./src/*"], + "#core/*": ["../../packages/core/*"] + } + }, + "include": ["./src/**/*", "./*.*ts"], + "exclude": ["**/node_modules/**/*", "./*.*js"] +} diff --git a/apps/catalog/.env.example b/apps/catalog/.env.example new file mode 100644 index 0000000..abbd38b --- /dev/null +++ b/apps/catalog/.env.example @@ -0,0 +1 @@ +CHROMATIC_PROJECT_TOKEN="" diff --git a/apps/catalog/.eslintignore b/apps/catalog/.eslintignore new file mode 100644 index 0000000..8433021 --- /dev/null +++ b/apps/catalog/.eslintignore @@ -0,0 +1,5 @@ +!.storybook/ +.storybook/static/ +coverage/ +generated/ +node_modules/ diff --git a/apps/catalog/.eslintrc.cjs b/apps/catalog/.eslintrc.cjs new file mode 100644 index 0000000..db62f50 --- /dev/null +++ b/apps/catalog/.eslintrc.cjs @@ -0,0 +1,4 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: ['lockerai-react'], +}; diff --git a/apps/catalog/.gitignore b/apps/catalog/.gitignore new file mode 100644 index 0000000..c41e5ac --- /dev/null +++ b/apps/catalog/.gitignore @@ -0,0 +1,2 @@ +# auto generated folder +generated/ diff --git a/apps/catalog/.lintstagedrc.cjs b/apps/catalog/.lintstagedrc.cjs new file mode 100644 index 0000000..6cf7dae --- /dev/null +++ b/apps/catalog/.lintstagedrc.cjs @@ -0,0 +1,5 @@ +module.exports = { + '**/*.{js,ts,tsx}': (/** @type {string[]} */ filenames) => `pnpm eslint --fix ${filenames.join(' --fix ')}`, + '**/*.{js,ts,tsx,json}': (/** @type {string[]} */ filenames) => `pnpm prettier --check ${filenames.join(' --check ')}`, + '**/*.{css,scss}': (/** @type {string[]} */ filenames) => `pnpm stylelint --fix ${filenames.join(' --fix ')}`, +}; diff --git a/apps/catalog/.prettierignore b/apps/catalog/.prettierignore new file mode 100644 index 0000000..55c4860 --- /dev/null +++ b/apps/catalog/.prettierignore @@ -0,0 +1,4 @@ +.storybook/static/ +coverage/ +generated/ +node_modules/ diff --git a/apps/catalog/.prettierrc.cjs b/apps/catalog/.prettierrc.cjs new file mode 100644 index 0000000..edc830e --- /dev/null +++ b/apps/catalog/.prettierrc.cjs @@ -0,0 +1,4 @@ +/** @type {import('prettier').Options} */ +module.exports = { + ...require('@lockerai/prettier'), +}; diff --git a/apps/catalog/.storybook/main.ts b/apps/catalog/.storybook/main.ts new file mode 100644 index 0000000..fba1ca1 --- /dev/null +++ b/apps/catalog/.storybook/main.ts @@ -0,0 +1,51 @@ +import path from 'path'; +import type { StorybookConfig } from '@storybook/nextjs'; + +const config: StorybookConfig = { + addons: ['@storybook/addon-a11y', '@storybook/addon-essentials', '@storybook/addon-styling'], + docs: { + autodocs: true, + }, + framework: { + name: '@storybook/nextjs', + options: {}, + }, + staticDirs: ['../../website/public'], + stories: [ + { + directory: '../../website/src', + files: '**/*.story.tsx', + titlePrefix: 'website', + }, + { + directory: '../../../packages/core', + files: '**/*.story.tsx', + titlePrefix: 'core', + }, + ], + typescript: { + reactDocgenTypescriptOptions: { + // NOTE: This setting is necessary to recognize JSDoc for components under monorepo. + // ref: https://github.com/storybookjs/storybook/issues/21399#issuecomment-1473800791 + include: ['../../../**/*.tsx'], + }, + }, + webpackFinal: (webpackConfig) => { + const finalConfig: typeof webpackConfig = { + ...webpackConfig, + resolve: { + ...webpackConfig.resolve, + alias: { + ...webpackConfig.resolve?.alias, + '~website': path.resolve(__dirname, '../../website'), + '#website': path.resolve(__dirname, '../../website/src'), + '#core': path.resolve(__dirname, '../../../packages/core'), + }, + }, + }; + + return finalConfig; + }, +}; + +export default config; diff --git a/apps/catalog/.storybook/manager.ts b/apps/catalog/.storybook/manager.ts new file mode 100644 index 0000000..9f3f9c3 --- /dev/null +++ b/apps/catalog/.storybook/manager.ts @@ -0,0 +1,6 @@ +import { addons } from '@storybook/addons'; +import { theme } from './theme'; + +addons.setConfig({ + theme, +}); diff --git a/apps/catalog/.storybook/preview.tsx b/apps/catalog/.storybook/preview.tsx new file mode 100644 index 0000000..9a252e0 --- /dev/null +++ b/apps/catalog/.storybook/preview.tsx @@ -0,0 +1,48 @@ +import { Image } from '@lockerai/core/component/image'; +import { ThemeProvider } from '@lockerai/core/component/theme-provider'; +import { firaCode, getFontVariables, notoSans } from '@lockerai/core/font/family'; +import { cn } from '@lockerai/tailwind'; +import { withThemeByDataAttribute } from '@storybook/addon-styling'; +import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'; +import type { Preview } from '@storybook/react'; +import React from 'react'; +import './storybook.css'; + +Image.defaultProps = { + unoptimized: true, +}; + +const themeDataAttribute = 'data-theme'; +const defaultTheme = 'light'; + +const preview: Preview = { + decorators: [ + (Story) => ( + + + + ), + // FIXME: I'm trying to add fontFamily to className and load the font by next/font, but oddly enough this does not work correctly. + // Probably due to the fact that it work on monorepo. + (Story) => ( +
+ +
+ ), + withThemeByDataAttribute({ + attributeName: themeDataAttribute, + themes: { + light: 'light', + dark: 'dark', + }, + defaultTheme: defaultTheme, + }), + ], + parameters: { + viewport: { + viewports: INITIAL_VIEWPORTS, + }, + }, +}; + +export default preview; diff --git a/apps/catalog/.storybook/storybook.css b/apps/catalog/.storybook/storybook.css new file mode 100644 index 0000000..0836e66 --- /dev/null +++ b/apps/catalog/.storybook/storybook.css @@ -0,0 +1 @@ +@import '@lockerai/tailwind/tailwind'; diff --git a/apps/catalog/.storybook/theme.ts b/apps/catalog/.storybook/theme.ts new file mode 100644 index 0000000..103eba9 --- /dev/null +++ b/apps/catalog/.storybook/theme.ts @@ -0,0 +1,31 @@ +import { getBaseUrl } from '@lockerai/core/util/get-base-url'; +import { create } from '@storybook/theming'; + +const brandLogo = ` + + + + + + + + + + + +`; + +export const theme = create({ + base: 'dark', + brandTitle: 'Locker.ai', + brandUrl: getBaseUrl({ app: 'website' }).toString(), + brandImage: `data:image/svg+xml;base64,${btoa(brandLogo)}`, +}); diff --git a/apps/catalog/.stylelintignore b/apps/catalog/.stylelintignore new file mode 100644 index 0000000..55c4860 --- /dev/null +++ b/apps/catalog/.stylelintignore @@ -0,0 +1,4 @@ +.storybook/static/ +coverage/ +generated/ +node_modules/ diff --git a/apps/catalog/.stylelintrc.cjs b/apps/catalog/.stylelintrc.cjs new file mode 100644 index 0000000..bae308b --- /dev/null +++ b/apps/catalog/.stylelintrc.cjs @@ -0,0 +1,4 @@ +/** @type {import('stylelint').Config} */ +module.exports = { + extends: ['@lockerai/stylelint'], +}; diff --git a/apps/catalog/jest.config.cjs b/apps/catalog/jest.config.cjs new file mode 100644 index 0000000..354e666 --- /dev/null +++ b/apps/catalog/jest.config.cjs @@ -0,0 +1,6 @@ +const config = require('@lockerai/jest/jest.node.cjs'); + +/** @type {import('jest').Config} */ +module.exports = { + ...config, +}; diff --git a/apps/catalog/package.json b/apps/catalog/package.json new file mode 100644 index 0000000..8270df8 --- /dev/null +++ b/apps/catalog/package.json @@ -0,0 +1,49 @@ +{ + "name": "@lockerai/catalog", + "version": "0.1.0", + "repository": "https://github.com/dino3616/lockerai.git", + "license": "MIT", + "private": true, + "type": "module", + "module": "nodenext", + "scripts": { + "lint": "eslint './**/{*.*js,*.*ts,*.*tsx}'", + "lint:fix": "eslint './**/{*.*js,*.*ts,*.*tsx}' --fix", + "fmt": "prettier --check .", + "fmt:fix": "prettier --write .", + "style": "stylelint ./{,.}**/*.{css,scss}", + "style:fix": "stylelint ./{,.}**/*.{css,scss} --fix", + "test": "jest --silent=false --verbose false --passWithNoTests", + "sb": "storybook dev --port 6006", + "sb:build": "storybook build -o ./.storybook/static", + "chromatic": "pnpx chromatic --project-token=$CHROMATIC_PROJECT_TOKEN" + }, + "dependencies": { + "@lockerai/core": "workspace:*", + "@lockerai/design-token": "workspace:*", + "@lockerai/tailwind": "workspace:*", + "dotenv": "16.3.1", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "devDependencies": { + "@lockerai/jest": "workspace:*", + "@lockerai/postcss": "workspace:*", + "@lockerai/prettier": "workspace:*", + "@lockerai/stylelint": "workspace:*", + "@lockerai/tsconfig": "workspace:*", + "@storybook/addon-a11y": "7.5.1", + "@storybook/addon-essentials": "7.5.1", + "@storybook/addon-styling": "1.3.7", + "@storybook/addon-viewport": "7.5.1", + "@storybook/addons": "7.5.1", + "@storybook/nextjs": "7.5.1", + "@storybook/react": "7.5.1", + "@storybook/theming": "7.5.1", + "@types/react": "18.2.31", + "@types/react-dom": "18.2.14", + "chromatic": "7.4.0", + "eslint-config-lockerai-react": "workspace:*", + "storybook": "7.5.1" + } +} diff --git a/apps/catalog/postcss.config.cjs b/apps/catalog/postcss.config.cjs new file mode 100644 index 0000000..f832f74 --- /dev/null +++ b/apps/catalog/postcss.config.cjs @@ -0,0 +1,3 @@ +module.exports = { + ...require('@lockerai/postcss'), +}; diff --git a/apps/catalog/tailwind.config.ts b/apps/catalog/tailwind.config.ts new file mode 100644 index 0000000..1a7cb1f --- /dev/null +++ b/apps/catalog/tailwind.config.ts @@ -0,0 +1,8 @@ +import { createConfig } from '@lockerai/tailwind/config'; + +const config = createConfig((defaultConfig) => ({ + ...defaultConfig, + content: ['./.storybook/**/*.{ts,tsx}', '../../**/*.{ts,tsx}', '!../../**/*.d.ts'], +})); + +export default config; diff --git a/apps/catalog/tsconfig.build.json b/apps/catalog/tsconfig.build.json new file mode 100644 index 0000000..1d20c3f --- /dev/null +++ b/apps/catalog/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/test/**/*", "**/*.*js", "**/*.spec.ts", "**/*.story.tsx"] +} diff --git a/apps/catalog/tsconfig.json b/apps/catalog/tsconfig.json new file mode 100644 index 0000000..a9afe3e --- /dev/null +++ b/apps/catalog/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@lockerai/tsconfig/tsconfig.nextjs.json", + "compilerOptions": { + "baseUrl": "./" + }, + "include": ["./*.*js", "./*.*ts", "./**/*.*ts", "./**/*.*tsx", "./.storybook/**/*"], + "exclude": ["./.storybook/static/**/*", "**/node_modules/**/*"] +} diff --git a/apps/website/.env.example b/apps/website/.env.example new file mode 100644 index 0000000..63393d6 --- /dev/null +++ b/apps/website/.env.example @@ -0,0 +1,3 @@ +NEXT_PUBLIC_GRAPHQL_ENDPOINT="https:///graphql" +NEXT_PUBLIC_WS_ENDPOINT="wss:///graphql" +REVALIDATE_TOKEN="" diff --git a/apps/website/.eslintignore b/apps/website/.eslintignore new file mode 100644 index 0000000..67461fd --- /dev/null +++ b/apps/website/.eslintignore @@ -0,0 +1,6 @@ +.next/ +.storybook/static/ +coverage/ +generated/ +node_modules/ +graphql.schema.json diff --git a/apps/website/.eslintrc.cjs b/apps/website/.eslintrc.cjs new file mode 100644 index 0000000..b9363d0 --- /dev/null +++ b/apps/website/.eslintrc.cjs @@ -0,0 +1,4 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: ['lockerai-nextjs'], +}; diff --git a/apps/website/.gitignore b/apps/website/.gitignore new file mode 100644 index 0000000..9d4c808 --- /dev/null +++ b/apps/website/.gitignore @@ -0,0 +1,3 @@ +# auto generated folder +generated/ +.vercel diff --git a/apps/website/.lintstagedrc.cjs b/apps/website/.lintstagedrc.cjs new file mode 100644 index 0000000..6cf7dae --- /dev/null +++ b/apps/website/.lintstagedrc.cjs @@ -0,0 +1,5 @@ +module.exports = { + '**/*.{js,ts,tsx}': (/** @type {string[]} */ filenames) => `pnpm eslint --fix ${filenames.join(' --fix ')}`, + '**/*.{js,ts,tsx,json}': (/** @type {string[]} */ filenames) => `pnpm prettier --check ${filenames.join(' --check ')}`, + '**/*.{css,scss}': (/** @type {string[]} */ filenames) => `pnpm stylelint --fix ${filenames.join(' --fix ')}`, +}; diff --git a/apps/website/.prettierignore b/apps/website/.prettierignore new file mode 100644 index 0000000..67461fd --- /dev/null +++ b/apps/website/.prettierignore @@ -0,0 +1,6 @@ +.next/ +.storybook/static/ +coverage/ +generated/ +node_modules/ +graphql.schema.json diff --git a/apps/website/.prettierrc.cjs b/apps/website/.prettierrc.cjs new file mode 100644 index 0000000..edc830e --- /dev/null +++ b/apps/website/.prettierrc.cjs @@ -0,0 +1,4 @@ +/** @type {import('prettier').Options} */ +module.exports = { + ...require('@lockerai/prettier'), +}; diff --git a/apps/website/.stylelintignore b/apps/website/.stylelintignore new file mode 100644 index 0000000..4bbb8cc --- /dev/null +++ b/apps/website/.stylelintignore @@ -0,0 +1,5 @@ +.next/ +.storybook/static/ +coverage/ +generated/ +node_modules/ diff --git a/apps/website/.stylelintrc.cjs b/apps/website/.stylelintrc.cjs new file mode 100644 index 0000000..bae308b --- /dev/null +++ b/apps/website/.stylelintrc.cjs @@ -0,0 +1,4 @@ +/** @type {import('stylelint').Config} */ +module.exports = { + extends: ['@lockerai/stylelint'], +}; diff --git a/apps/website/codegen-introspect.ts b/apps/website/codegen-introspect.ts new file mode 100644 index 0000000..a8271c8 --- /dev/null +++ b/apps/website/codegen-introspect.ts @@ -0,0 +1,14 @@ +import type { CodegenConfig } from '@graphql-codegen/cli'; + +const config: CodegenConfig = { + overwrite: true, + schema: process.env['NEXT_PUBLIC_GRAPHQL_ENDPOINT'] || 'http://localhost:4000/graphql', + documents: ['src/infra/graphql/document/**/*.gql'], + generates: { + './graphql.schema.json': { + plugins: ['introspection'], + }, + }, +}; + +export default config; diff --git a/apps/website/codegen.ts b/apps/website/codegen.ts new file mode 100644 index 0000000..8275041 --- /dev/null +++ b/apps/website/codegen.ts @@ -0,0 +1,21 @@ +import type { CodegenConfig } from '@graphql-codegen/cli'; + +const config: CodegenConfig = { + overwrite: true, + schema: './graphql.schema.json', + documents: ['src/infra/graphql/document/**/*.gql'], + generates: { + './src/infra/graphql/generated/graphql.ts': { + config: { + scalars: { + DateTime: 'Date', + }, + strictScalars: true, + withHooks: false, + }, + plugins: ['typescript', 'typescript-operations', 'typescript-urql'], + }, + }, +}; + +export default config; diff --git a/apps/website/graphql.schema.json b/apps/website/graphql.schema.json new file mode 100644 index 0000000..cb1b835 --- /dev/null +++ b/apps/website/graphql.schema.json @@ -0,0 +1,1323 @@ +{ + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": null, + "types": [ + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "DateTime", + "description": "A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Mutation", + "description": null, + "fields": [ + { + "name": "createUser", + "description": null, + "args": [ + { + "name": "user", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UserCreateInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [ + { + "name": "findUser", + "description": null, + "args": [ + { + "name": "where", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UserWhereUniqueInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "User", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "User", + "description": null, + "fields": [ + { + "name": "authId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fingerprintId", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lostAndFoundState", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "UserLostAndFoundState", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UserCreateInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "authId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "UserLostAndFoundState", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "DELIVERING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NONE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RETRIEVING", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UserWhereUniqueInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "userId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isRepeatable", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": "Location adjacent to a variable definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specifiedByURL", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "isRepeatable": false, + "locations": [ + "ARGUMENT_DEFINITION", + "ENUM_VALUE", + "FIELD_DEFINITION", + "INPUT_FIELD_DEFINITION" + ], + "args": [ + { + "name": "reason", + "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"", + "isDeprecated": false, + "deprecationReason": null + } + ] + }, + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "isRepeatable": false, + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "isRepeatable": false, + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + }, + { + "name": "specifiedBy", + "description": "Exposes a URL that specifies the behavior of this scalar.", + "isRepeatable": false, + "locations": [ + "SCALAR" + ], + "args": [ + { + "name": "url", + "description": "The URL that specifies the behavior of this scalar.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + } + ] + } +} \ No newline at end of file diff --git a/apps/website/jest.config.cjs b/apps/website/jest.config.cjs new file mode 100644 index 0000000..b31295a --- /dev/null +++ b/apps/website/jest.config.cjs @@ -0,0 +1,10 @@ +/** @type {(overrideConfig: import('jest').Config) => Promise} */ +const createConfig = require('@lockerai/jest/jest.nextjs.cjs'); + +module.exports = createConfig({ + moduleNameMapper: { + '^~website/(.*)$': '/$1', + '^#website/(.*)$': '/src/$1', + '^#core/(.*)$': '/../../packages/core/$1', + }, +}); diff --git a/apps/website/next-env.d.ts b/apps/website/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/apps/website/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/website/next.config.mjs b/apps/website/next.config.mjs new file mode 100644 index 0000000..ef4be5e --- /dev/null +++ b/apps/website/next.config.mjs @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const config = { + experimental: { + typedRoutes: true, + }, + reactStrictMode: true, + transpilePackages: ['@lockerai/core'], +}; + +export default config; diff --git a/apps/website/package.json b/apps/website/package.json new file mode 100644 index 0000000..3efaf96 --- /dev/null +++ b/apps/website/package.json @@ -0,0 +1,55 @@ +{ + "name": "@lockerai/website", + "version": "0.1.0", + "repository": "https://github.com/dino3616/lockerai.git", + "license": "MIT", + "private": true, + "type": "module", + "module": "nodenext", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "lint:fix": "next lint --fix", + "fmt": "prettier --check .", + "fmt:fix": "prettier --write .", + "style": "stylelint ./**/*.{css,scss}", + "style:fix": "stylelint ./**/*.{css,scss} --fix", + "test": "jest --silent=false --verbose false --passWithNoTests", + "gql:introspect": "graphql-codegen --config codegen-introspect.ts -r dotenv/config" + }, + "dependencies": { + "@lockerai/core": "workspace:*", + "@lockerai/design-token": "workspace:*", + "@lockerai/tailwind": "workspace:*", + "@lockerai/urql": "workspace:*", + "dotenv": "16.3.1", + "framer-motion": "10.16.4", + "graphql-tag": "2.12.6", + "next": "13.5.6", + "react": "18.2.0", + "react-dom": "18.2.0", + "schema-dts": "1.1.2", + "sharp": "0.32.6", + "ts-pattern": "5.0.5" + }, + "devDependencies": { + "@graphql-codegen/cli": "5.0.0", + "@graphql-codegen/introspection": "4.0.0", + "@graphql-codegen/typescript": "4.0.1", + "@graphql-codegen/typescript-operations": "4.0.1", + "@graphql-codegen/typescript-urql": "4.0.0", + "@graphql-codegen/urql-introspection": "3.0.0", + "@lockerai/jest": "workspace:*", + "@lockerai/postcss": "workspace:*", + "@lockerai/prettier": "workspace:*", + "@lockerai/stylelint": "workspace:*", + "@lockerai/tsconfig": "workspace:*", + "@lockerai/type": "workspace:*", + "@storybook/react": "7.5.1", + "@types/react": "18.2.31", + "@types/react-dom": "18.2.14", + "eslint-config-lockerai-nextjs": "workspace:*" + } +} diff --git a/apps/website/postcss.config.cjs b/apps/website/postcss.config.cjs new file mode 100644 index 0000000..f832f74 --- /dev/null +++ b/apps/website/postcss.config.cjs @@ -0,0 +1,3 @@ +module.exports = { + ...require('@lockerai/postcss'), +}; diff --git a/apps/website/public/.gitkeep b/apps/website/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/website/src/app/apple-icon.png b/apps/website/src/app/apple-icon.png new file mode 100644 index 0000000..14e7cce Binary files /dev/null and b/apps/website/src/app/apple-icon.png differ diff --git a/apps/website/src/app/favicon.ico b/apps/website/src/app/favicon.ico new file mode 100644 index 0000000..078f075 Binary files /dev/null and b/apps/website/src/app/favicon.ico differ diff --git a/apps/website/src/app/icon.png b/apps/website/src/app/icon.png new file mode 100644 index 0000000..445665d Binary files /dev/null and b/apps/website/src/app/icon.png differ diff --git a/apps/website/src/app/json-ld.ts b/apps/website/src/app/json-ld.ts new file mode 100644 index 0000000..93adb64 --- /dev/null +++ b/apps/website/src/app/json-ld.ts @@ -0,0 +1,35 @@ +import { getBaseUrl } from '@lockerai/core/util/get-base-url'; +import type { WebApplication, WithContext } from 'schema-dts'; + +export const jsonLd: WithContext = { + '@context': 'https://schema.org', + '@type': 'WebApplication', + '@id': getBaseUrl({ app: 'website' }).toString(), + applicationCategory: ['LifestyleApplication', 'SecurityApplication', 'UtilitiesApplication'], + applicationSubCategory: ['LostAndFound'], + browserRequirements: 'Requires JavaScript. Requires HTML5.', + description: 'Locker.ai is a service that uses a unique AI-driven authentication mechanism to safely report and retrieve lost items.', + featureList: ['Search for lost items', 'Register lost items'], + genre: ['Lifestyle', 'Security', 'Utilities', 'Lost and Found'], + image: `${getBaseUrl({ app: 'website' })}/ogp.png`, + inLanguage: 'en', + license: 'https://creativecommons.org/licenses/by/4.0', + maintainer: { + '@type': 'Organization', + name: 'NITIC PBL P8', + url: 'https://github.com/nitic-pbl-p8', + }, + name: 'Locker.ai', + offers: { + '@type': 'Offer', + price: '0', + }, + provider: { + '@type': 'Organization', + name: 'NITIC PBL P8', + url: 'https://github.com/nitic-pbl-p8', + }, + // TODO: Take a screenshot of the /dashboard page and publish it. + screenshot: '', + url: getBaseUrl({ app: 'website' }).toString(), +}; diff --git a/apps/website/src/app/layout.tsx b/apps/website/src/app/layout.tsx new file mode 100644 index 0000000..d5772cb --- /dev/null +++ b/apps/website/src/app/layout.tsx @@ -0,0 +1,63 @@ +import { ThemeProvider } from '@lockerai/core/component/theme-provider'; +import { firaCode, getFontVariables, notoSans } from '@lockerai/core/font/family'; +import { getBaseUrl } from '@lockerai/core/util/get-base-url'; +import { colors } from '@lockerai/design-token'; +import { cn } from '@lockerai/tailwind'; +import type { Metadata, NextPage } from 'next'; +import type { ReactNode } from 'react'; +import { Footer } from '#website/common/component/footer'; +import { Header } from '#website/common/component/header'; +import '#website/style/global.css'; + +type RootLayoutProps = { + children: ReactNode; +}; + +const RootLayout: NextPage = ({ children }) => ( + + + +
+ +
+ {children} +