initial
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.idea
|
||||||
|
.turbo
|
||||||
|
.git
|
||||||
|
.gitab-ci.d
|
||||||
|
.gitlab-ci.yml
|
||||||
|
out
|
||||||
|
**/node_modules
|
||||||
|
Dockerfile.*
|
||||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# turbo
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
/.idea/git_toolbox_prj.xml
|
||||||
|
/.idea/vcs.xml
|
||||||
45
.gitlab-ci.d/app-client-cf-pages.yml
Normal file
45
.gitlab-ci.d/app-client-cf-pages.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
build-app-client-cf-pages:
|
||||||
|
stage: build
|
||||||
|
needs: []
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||||
|
image:
|
||||||
|
name: gcr.io/kaniko-project/executor:v1.15.0-debug
|
||||||
|
entrypoint: [""]
|
||||||
|
script:
|
||||||
|
- >-
|
||||||
|
/kaniko/executor
|
||||||
|
--context "${CI_PROJECT_DIR}"
|
||||||
|
--dockerfile "${CI_PROJECT_DIR}/Dockerfile.client"
|
||||||
|
--target builder-cloudflare
|
||||||
|
--skip-unused-stages=true
|
||||||
|
--cache=true
|
||||||
|
--cache-repo "${CI_REGISTRY_IMAGE}/frontend-app-client/cache"
|
||||||
|
--no-push
|
||||||
|
|
||||||
|
deploy-app-client-cf-pages:
|
||||||
|
stage: deploy
|
||||||
|
needs: []
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "ci/pay"
|
||||||
|
changes:
|
||||||
|
- apps/client/**/*
|
||||||
|
- layers/ui/**/*
|
||||||
|
- layers/shared/**/*
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "ci/pay"
|
||||||
|
when: manual
|
||||||
|
image:
|
||||||
|
name: gcr.io/kaniko-project/executor:v1.15.0-debug
|
||||||
|
entrypoint: [""]
|
||||||
|
script:
|
||||||
|
- cp ${CLOUDFLARE_PRGMS_IO_ACCOUNT_ID} /kaniko/cloudflare-account-id
|
||||||
|
- cp ${CLOUDFLARE_PRGMS_IO_WRANGLER_API_TOKEN} /kaniko/cloudflare-api-token
|
||||||
|
- >-
|
||||||
|
/kaniko/executor
|
||||||
|
--context "${CI_PROJECT_DIR}"
|
||||||
|
--dockerfile "${CI_PROJECT_DIR}/Dockerfile.client"
|
||||||
|
--target deployer-cloudflare
|
||||||
|
--skip-unused-stages=true
|
||||||
|
--cache=true
|
||||||
|
--cache-repo "${CI_REGISTRY_IMAGE}/frontend-app-client/cache"
|
||||||
|
--no-push
|
||||||
89
.gitlab-ci.d/app-client-docker.yml
Normal file
89
.gitlab-ci.d/app-client-docker.yml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
build-app-client-docker:
|
||||||
|
stage: build
|
||||||
|
needs: []
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||||
|
image:
|
||||||
|
name: gcr.io/kaniko-project/executor:v1.15.0-debug
|
||||||
|
entrypoint: [""]
|
||||||
|
script:
|
||||||
|
- >-
|
||||||
|
/kaniko/executor
|
||||||
|
--context "${CI_PROJECT_DIR}"
|
||||||
|
--dockerfile "${CI_PROJECT_DIR}/Dockerfile.client"
|
||||||
|
--target runner-nodejs
|
||||||
|
--skip-unused-stages=true
|
||||||
|
--cache=true
|
||||||
|
--cache-repo "${CI_REGISTRY_IMAGE}/frontend-app-client/cache"
|
||||||
|
--no-push
|
||||||
|
|
||||||
|
build-app-client-docker-latest:
|
||||||
|
stage: build
|
||||||
|
needs: []
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "ci/pay"
|
||||||
|
changes:
|
||||||
|
- apps/client/**/*
|
||||||
|
- layers/ui/**/*
|
||||||
|
- layers/shared/**/*
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "ci/pay"
|
||||||
|
when: manual
|
||||||
|
image:
|
||||||
|
name: gcr.io/kaniko-project/executor:v1.15.0-debug
|
||||||
|
entrypoint: [""]
|
||||||
|
script:
|
||||||
|
- >-
|
||||||
|
/kaniko/executor
|
||||||
|
--context "${CI_PROJECT_DIR}"
|
||||||
|
--dockerfile "${CI_PROJECT_DIR}/Dockerfile.client"
|
||||||
|
--target runner-nodejs
|
||||||
|
--skip-unused-stages=true
|
||||||
|
--cache=true
|
||||||
|
--cache-repo "${CI_REGISTRY_IMAGE}/frontend-app-client/cache"
|
||||||
|
--destination "${CI_REGISTRY_IMAGE}/frontend-app-client:latest"
|
||||||
|
--image-name-with-digest-file "${CI_PROJECT_DIR}/.images/frontend-app-client.txt"
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- .images/frontend-app-client.txt
|
||||||
|
|
||||||
|
deploy-app-client-docker:
|
||||||
|
image: alpine:latest
|
||||||
|
stage: deploy
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "ci/pay"
|
||||||
|
needs:
|
||||||
|
- job: build-app-client-docker-latest
|
||||||
|
artifacts: true
|
||||||
|
timeout: 3m
|
||||||
|
variables:
|
||||||
|
DOCKER_RUN_COMMAND:
|
||||||
|
expand: true
|
||||||
|
value: >-
|
||||||
|
docker run
|
||||||
|
--name frontend-app
|
||||||
|
--detach
|
||||||
|
--restart unless-stopped
|
||||||
|
--network frontend
|
||||||
|
-e NITRO_SHUTDOWN=true
|
||||||
|
-e NICONSOLE_LEVEL=4
|
||||||
|
-e CONSOLA_LEVEL=4
|
||||||
|
-e NITRO_API_HOST=https://api.prgms.io/api/v1
|
||||||
|
"$(cat frontend-app-client.txt)"
|
||||||
|
before_script:
|
||||||
|
- apk add openssh-client
|
||||||
|
- eval $(ssh-agent -s)
|
||||||
|
- chmod 400 "$ID_RSA"
|
||||||
|
- ssh-add "$ID_RSA"
|
||||||
|
- mkdir -p ~/.ssh
|
||||||
|
- chmod 700 ~/.ssh
|
||||||
|
- cp "$SSH_KNOWN_HOSTS" ~/.ssh/known_hosts
|
||||||
|
script:
|
||||||
|
- test -f .images/frontend-app-client.txt
|
||||||
|
- scp .images/frontend-app-client.txt ${SERVER_USER}@${SERVER_IP}:frontend-app-client.txt
|
||||||
|
- >-
|
||||||
|
ssh
|
||||||
|
"$SERVER_USER@$SERVER_IP"
|
||||||
|
"docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY"
|
||||||
|
- ssh "${SERVER_USER}@${SERVER_IP}" "docker stop frontend-app || true"
|
||||||
|
- ssh "${SERVER_USER}@${SERVER_IP}" "docker rm frontend-app || true"
|
||||||
|
- ssh "${SERVER_USER}@${SERVER_IP}" "${DOCKER_RUN_COMMAND}"
|
||||||
45
.gitlab-ci.d/app-pay-cf-pages.yml
Normal file
45
.gitlab-ci.d/app-pay-cf-pages.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
build-app-pay-cf-pages:
|
||||||
|
stage: build
|
||||||
|
needs: []
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||||
|
image:
|
||||||
|
name: gcr.io/kaniko-project/executor:v1.15.0-debug
|
||||||
|
entrypoint: [""]
|
||||||
|
script:
|
||||||
|
- >-
|
||||||
|
/kaniko/executor
|
||||||
|
--context "${CI_PROJECT_DIR}"
|
||||||
|
--dockerfile "${CI_PROJECT_DIR}/Dockerfile.pay"
|
||||||
|
--target builder-cloudflare
|
||||||
|
--skip-unused-stages=true
|
||||||
|
--cache=true
|
||||||
|
--cache-repo "${CI_REGISTRY_IMAGE}/frontend-app-pay/cache"
|
||||||
|
--no-push
|
||||||
|
|
||||||
|
deploy-app-pay-cf-pages:
|
||||||
|
stage: deploy
|
||||||
|
needs: []
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "ci/pay"
|
||||||
|
changes:
|
||||||
|
- apps/pay/**/*
|
||||||
|
- layers/ui/**/*
|
||||||
|
- layers/shared/**/*
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "ci/pay"
|
||||||
|
when: manual
|
||||||
|
image:
|
||||||
|
name: gcr.io/kaniko-project/executor:v1.15.0-debug
|
||||||
|
entrypoint: [""]
|
||||||
|
script:
|
||||||
|
- cp ${CLOUDFLARE_PRGMS_IO_ACCOUNT_ID} /kaniko/cloudflare-account-id
|
||||||
|
- cp ${CLOUDFLARE_PRGMS_IO_WRANGLER_API_TOKEN} /kaniko/cloudflare-api-token
|
||||||
|
- >-
|
||||||
|
/kaniko/executor
|
||||||
|
--context "${CI_PROJECT_DIR}"
|
||||||
|
--dockerfile "${CI_PROJECT_DIR}/Dockerfile.pay"
|
||||||
|
--target deployer-cloudflare
|
||||||
|
--skip-unused-stages=true
|
||||||
|
--cache=true
|
||||||
|
--cache-repo "${CI_REGISTRY_IMAGE}/frontend-app-pay/cache"
|
||||||
|
--no-push
|
||||||
52
.gitlab-ci.yml
Normal file
52
.gitlab-ci.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
include:
|
||||||
|
- local: .gitlab-ci.d/app-client-docker.yml
|
||||||
|
- local: .gitlab-ci.d/app-client-cf-pages.yml
|
||||||
|
- local: .gitlab-ci.d/app-pay-cf-pages.yml
|
||||||
|
|
||||||
|
report-deploy-client-success: ¬ificationJob
|
||||||
|
image: alpine:latest
|
||||||
|
stage: .post
|
||||||
|
when: on_success
|
||||||
|
needs:
|
||||||
|
- deploy-app-client-docker
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main"
|
||||||
|
- if: $CI_COMMIT_BRANCH == "ci/pay"
|
||||||
|
before_script:
|
||||||
|
- apk add curl
|
||||||
|
variables:
|
||||||
|
NOTIFICATION_TEXT: "Frontend successfully deployed, check at https://app.prgms.io"
|
||||||
|
script:
|
||||||
|
- >-
|
||||||
|
curl -s -S -X POST
|
||||||
|
"https://api.telegram.org/bot${TELEGRAM_DEPLOY_NOTIFY_TOKEN}/sendMessage"
|
||||||
|
-d "chat_id=${TELEGRAM_DEPLOY_NOTIFY_CHAT_ID}"
|
||||||
|
-d text="$NOTIFICATION_TEXT"
|
||||||
|
|
||||||
|
report-deploy-client-failure:
|
||||||
|
<<: *notificationJob
|
||||||
|
when: on_failure
|
||||||
|
needs:
|
||||||
|
- deploy-app-client-docker
|
||||||
|
variables:
|
||||||
|
NOTIFICATION_TEXT: "Frontend deployment failed, check pipeline at ${CI_PIPELINE_URL}"
|
||||||
|
|
||||||
|
report-deploy-pay-success:
|
||||||
|
<<: *notificationJob
|
||||||
|
when: on_success
|
||||||
|
needs:
|
||||||
|
- deploy-app-pay-cf-pages
|
||||||
|
variables:
|
||||||
|
NOTIFICATION_TEXT: "Payment page successfully deployed, check at https://pay.prgms.io"
|
||||||
|
|
||||||
|
report-deploy-pay-failure:
|
||||||
|
<<: *notificationJob
|
||||||
|
when: on_failure
|
||||||
|
needs:
|
||||||
|
- deploy-app-pay-cf-pages
|
||||||
|
variables:
|
||||||
|
NOTIFICATION_TEXT: "Payment page deployment failed, check pipeline at ${CI_PIPELINE_URL}"
|
||||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/git_toolbox_blame.xml
generated
Normal file
6
.idea/git_toolbox_blame.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GitToolBoxBlameSettings">
|
||||||
|
<option name="version" value="2" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
14
.idea/indefiti.iml
generated
Normal file
14
.idea/indefiti.iml
generated
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/apps/client/.output" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/apps/client/.turbo" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
23
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
23
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myValues">
|
||||||
|
<value>
|
||||||
|
<list size="7">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="nobr" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="noembed" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="comment" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="noscript" />
|
||||||
|
<item index="4" class="java.lang.String" itemvalue="embed" />
|
||||||
|
<item index="5" class="java.lang.String" itemvalue="script" />
|
||||||
|
<item index="6" class="java.lang.String" itemvalue="uiiconbell" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="myCustomValuesEnabled" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SassScssResolvedByNameOnly" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptLibraryMappings">
|
||||||
|
<includedPredefinedLibrary name="Node.js Core" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/jsLinters/eslint.xml
generated
Normal file
6
.idea/jsLinters/eslint.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EslintConfiguration">
|
||||||
|
<option name="fix-on-save" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/indefiti.iml" filepath="$PROJECT_DIR$/.idea/indefiti.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
4
.idea/watcherTasks.xml
generated
Normal file
4
.idea/watcherTasks.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectTasksOptions" suppressed-tasks="SCSS" />
|
||||||
|
</project>
|
||||||
7
.prettierrc.js
Normal file
7
.prettierrc.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
endOfLine: 'auto',
|
||||||
|
trailingComma: 'es5',
|
||||||
|
tabWidth: 2,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
};
|
||||||
46
Dockerfile.client
Normal file
46
Dockerfile.client
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
FROM node:18-alpine AS base
|
||||||
|
WORKDIR /app
|
||||||
|
RUN yarn global add turbo@1.5.5
|
||||||
|
|
||||||
|
FROM base AS pruner
|
||||||
|
COPY . .
|
||||||
|
RUN test -f package.json
|
||||||
|
RUN turbo prune --scope=client --docker
|
||||||
|
|
||||||
|
# Add lockfile and package.json's of isolated subworkspace
|
||||||
|
FROM base AS deps
|
||||||
|
COPY --link --from=pruner /app/out/json/ .
|
||||||
|
COPY --link --from=pruner /app/out/yarn.lock ./yarn.lock
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
|
COPY --from=pruner /app/out/full .
|
||||||
|
|
||||||
|
FROM deps AS builder-cloudflare
|
||||||
|
ENV NITRO_PRESET=cloudflare-pages
|
||||||
|
RUN turbo run build --scope=client
|
||||||
|
|
||||||
|
FROM node:18-alpine AS deployer-cloudflare
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm install --global wrangler
|
||||||
|
COPY --link --from=builder-cloudflare /app/apps/client/dist /app/dist
|
||||||
|
ARG GIT_BRANCH="main" \
|
||||||
|
GIT_COMMIT="" \
|
||||||
|
CF_PAGES_PROJECT="app-prgms-io"
|
||||||
|
RUN --mount=type=secret,id=cloudflare-account-id,target=/kaniko/cloudflare-account-id \
|
||||||
|
--mount=type=secret,id=cloudflare-api-token,target=/kaniko/cloudflare-api-token \
|
||||||
|
CLOUDFLARE_ACCOUNT_ID=$(cat /kaniko/cloudflare-account-id) \
|
||||||
|
CLOUDFLARE_API_TOKEN=$(cat /kaniko/cloudflare-api-token) \
|
||||||
|
wrangler pages deploy \
|
||||||
|
--project-name "${CF_PAGES_PROJECT}" \
|
||||||
|
--branch "${GIT_BRANCH}" \
|
||||||
|
--commit-hash "${GIT_COMMIT}" \
|
||||||
|
dist
|
||||||
|
|
||||||
|
FROM deps AS builder-nodejs
|
||||||
|
ENV NITRO_PRESET=node-server
|
||||||
|
RUN turbo run build --scope=client
|
||||||
|
|
||||||
|
FROM node:18-alpine AS runner-nodejs
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder-nodejs /app/apps/client/.output /app
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD [ "node", "./server/index.mjs" ]
|
||||||
45
Dockerfile.pay
Normal file
45
Dockerfile.pay
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
FROM node:18-alpine AS base
|
||||||
|
WORKDIR /app
|
||||||
|
RUN yarn global add turbo@1.5.5
|
||||||
|
|
||||||
|
FROM base AS pruner
|
||||||
|
COPY . .
|
||||||
|
RUN test -f package.json
|
||||||
|
RUN turbo prune --scope=pay --docker
|
||||||
|
|
||||||
|
FROM base as deps
|
||||||
|
COPY --link --from=pruner /app/out/json/ .
|
||||||
|
COPY --link --from=pruner /app/out/yarn.lock ./yarn.lock
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
|
COPY --from=pruner /app/out/full .
|
||||||
|
|
||||||
|
FROM deps AS builder-cloudflare
|
||||||
|
ENV NITRO_PRESET=cloudflare-pages
|
||||||
|
RUN turbo run build --scope=pay
|
||||||
|
|
||||||
|
FROM node:18-alpine AS deployer-cloudflare
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm install --global wrangler
|
||||||
|
COPY --link --from=builder-cloudflare /app/apps/pay/dist /app/dist
|
||||||
|
ARG GIT_BRANCH="main" \
|
||||||
|
GIT_COMMIT="" \
|
||||||
|
CF_PAGES_PROJECT="pay-prgms-io"
|
||||||
|
RUN --mount=type=secret,id=cloudflare-account-id,target=/kaniko/cloudflare-account-id \
|
||||||
|
--mount=type=secret,id=cloudflare-api-token,target=/kaniko/cloudflare-api-token \
|
||||||
|
CLOUDFLARE_ACCOUNT_ID=$(cat /kaniko/cloudflare-account-id) \
|
||||||
|
CLOUDFLARE_API_TOKEN=$(cat /kaniko/cloudflare-api-token) \
|
||||||
|
wrangler pages deploy \
|
||||||
|
--project-name "${CF_PAGES_PROJECT}" \
|
||||||
|
--branch "${GIT_BRANCH}" \
|
||||||
|
--commit-hash "${GIT_COMMIT}" \
|
||||||
|
dist
|
||||||
|
|
||||||
|
FROM deps AS builder-nodejs
|
||||||
|
ENV NITRO_PRESET=node-server
|
||||||
|
RUN turbo run build --scope=pay
|
||||||
|
|
||||||
|
FROM node:18-alpine AS runner-nodejs
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder-nodejs /app/apps/pay/.output /app
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD [ "node", "./server/index.mjs" ]
|
||||||
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Cryptokittens Front
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
- `apps`
|
||||||
|
- `client` - основное приложение
|
||||||
|
- `pay` - страница оплаты
|
||||||
|
- `admin` - админка (возможно когда-то будет?)
|
||||||
|
- `packages`
|
||||||
|
- `ui`- библиотека UI компонентов
|
||||||
|
- `eslint-config-custom` - конфигурация для ESLint и Prettier
|
||||||
|
|
||||||
|
## Режим разработчика
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn
|
||||||
|
yarn dev:client
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сборка проекта
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn
|
||||||
|
yarn build:client
|
||||||
|
```
|
||||||
|
|
||||||
|
Итоговый код будет доступен по пути `/apps/client/.output`
|
||||||
|
|
||||||
24
apps/client/.gitignore
vendored
Normal file
24
apps/client/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
2
apps/client/.npmrc
Normal file
2
apps/client/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
shamefully-hoist=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
13
apps/client/app.vue
Normal file
13
apps/client/app.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtLoadingIndicator :height="4" />
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
useHead({
|
||||||
|
title: 'Indefiti',
|
||||||
|
meta: [{ name: 'robots', content: 'noindex,nofollow' }],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
7
apps/client/assets/styles.scss
Normal file
7
apps/client/assets/styles.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
body, html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#__nuxt {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
165
apps/client/components/asset/card.vue
Normal file
165
apps/client/components/asset/card.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="asset-card" :class="{ 'is-active': isActive, 'is-disabled': disabled }">
|
||||||
|
<UiCoin
|
||||||
|
class="asset-card__coin"
|
||||||
|
:code="code"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="asset-card__name-wrapper">
|
||||||
|
<p class="asset-card__code">
|
||||||
|
{{ code }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="asset-card__name">
|
||||||
|
{{ name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="asset-card__money">
|
||||||
|
<p class="asset-card__balance">
|
||||||
|
<span
|
||||||
|
v-if="showConverted"
|
||||||
|
class="asset-card__converted"
|
||||||
|
>
|
||||||
|
{{ convertedBalance }}
|
||||||
|
</span>
|
||||||
|
<span>{{ balance }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
v-if="withdraw"
|
||||||
|
class="asset-card__withdraw"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="showConverted"
|
||||||
|
class="asset-card__converted"
|
||||||
|
>
|
||||||
|
{{ convertedWithdraw }}
|
||||||
|
</span>
|
||||||
|
<span>{{ withdraw }}</span>
|
||||||
|
<UiIconSUpRight class="asset-card__withdraw-icon" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type Decimal from 'decimal.js'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
code: string
|
||||||
|
name?: string
|
||||||
|
balance: Decimal.Value
|
||||||
|
withdraw?: Decimal.Value
|
||||||
|
rate: Decimal.Value
|
||||||
|
isActive?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const showConverted = computed(() => !!props.rate && props.rate !== 1)
|
||||||
|
const convertedBalance = computed(() => $money.fullFormat($money.convert(props.balance, props.rate), 'USDT'))
|
||||||
|
const convertedWithdraw = computed(() => $money.fullFormat($money.convert(props.balance, props.rate), 'USDT'))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.asset-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr 1fr;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: $clr-grey-100;
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
outline-offset: -2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
transition-property: outline-color, background-color;
|
||||||
|
min-height: 97px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
outline-color: $clr-grey-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active,
|
||||||
|
&:active {
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
outline-color: $clr-cyan-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name-wrapper {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__coin {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__code {
|
||||||
|
@include txt-l-sb;
|
||||||
|
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
@include txt-r-m;
|
||||||
|
|
||||||
|
margin-top: 2px;
|
||||||
|
color: $clr-grey-400;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__money {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__balance {
|
||||||
|
@include txt-l-sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__withdraw {
|
||||||
|
@include txt-i-sb;
|
||||||
|
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__balance,
|
||||||
|
&__withdraw {
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
span {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__withdraw-icon {
|
||||||
|
color: $clr-cyan-500;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__converted {
|
||||||
|
@include txt-i-m;
|
||||||
|
|
||||||
|
vertical-align: middle;
|
||||||
|
color: $clr-grey-400;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '/';
|
||||||
|
margin-inline: 4px;
|
||||||
|
color: $clr-grey-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
73
apps/client/components/asset/sidebar.vue
Normal file
73
apps/client/components/asset/sidebar.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<Sidebar id="asset" class="asset-sidebar" @close="$emit('close')">
|
||||||
|
<UiCoin :code="asset.code" class="asset-sidebar__coin" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<MoneyAmount class="asset-sidebar__balance" :value="asset.balance" :currency="asset.code" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="asset-sidebar__actions">
|
||||||
|
<UiButton type="ghost" size="small" icon="s-up-right" :href="`/create/withdraw/${projectId}/${asset.code}`">
|
||||||
|
Отправить
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<UiButton type="ghost" size="small" icon="s-down-left" :href="`/create/invoice/${projectId}`">
|
||||||
|
Получить
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<UiButton type="ghost" color="secondary" size="small" icon="s-exchange" disabled>
|
||||||
|
Обменять
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</Sidebar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Sidebar } from '#components'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
projectId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(['close'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.asset-sidebar {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__coin {
|
||||||
|
border-radius: 9px;
|
||||||
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__balance {
|
||||||
|
@include h3('money-amount-value', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
--button-ghost-primary-color: #{$clr-black};
|
||||||
|
--button-icon-color: #{$clr-cyan-500};
|
||||||
|
--button-icon-disabled-color: #{$clr-grey-400};
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
padding: 4px;
|
||||||
|
background-color: $clr-grey-100;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-top: 40px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
107
apps/client/components/authorization/email-confirmation.vue
Normal file
107
apps/client/components/authorization/email-confirmation.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div class="email-confirmation">
|
||||||
|
<img src="/email-confirmation.svg" alt="logo" draggable="false">
|
||||||
|
|
||||||
|
<h1 class="email-confirmation__title">
|
||||||
|
{{ $t('check_email') }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="email-confirmation__content">
|
||||||
|
<i18n-t
|
||||||
|
scope="global"
|
||||||
|
keypath="we_have_sent_you_an_email_to"
|
||||||
|
tag="p"
|
||||||
|
class="email-confirmation__text"
|
||||||
|
>
|
||||||
|
<strong class="email-confirmation__email">{{ email }}</strong>
|
||||||
|
</i18n-t>
|
||||||
|
|
||||||
|
<div class="email-confirmation__instructions">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="email-confirmation__upper-text">
|
||||||
|
<i18n-t class="mb-18" keypath="check_spam_folder" tag="p" scope="global">
|
||||||
|
<strong class="text-grey-600">«{{ $t('spam') }}»</strong>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="resendFn" class="email-confirmation__resend">
|
||||||
|
<span>{{ $t('did_not_get_mail') }}</span>
|
||||||
|
|
||||||
|
<UiButton class="ml-4" type="link" @click="resendFn">
|
||||||
|
{{ $t('send_again') }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
email: {
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
resendFn: { type: Function },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.email-confirmation {
|
||||||
|
width: 516px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
@include txt-l;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__instructions {
|
||||||
|
@include txt-l-sb;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__email {
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__upper-text {
|
||||||
|
@include txt-m;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 40px;
|
||||||
|
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__resend {
|
||||||
|
@include txt-i-m;
|
||||||
|
|
||||||
|
color: $clr-grey-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__under-text {
|
||||||
|
@include txt-i-m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
48
apps/client/components/authorization/faq-password.vue
Normal file
48
apps/client/components/authorization/faq-password.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<UiAccordion>
|
||||||
|
<UiAccordionItem
|
||||||
|
:title="$t('forgot_password_questions.forgot_password.title')"
|
||||||
|
>
|
||||||
|
<i18n-t
|
||||||
|
scope="global"
|
||||||
|
keypath="forgot_password_questions.forgot_password.content"
|
||||||
|
tag="p"
|
||||||
|
>
|
||||||
|
<UiButton type="link" href="/reset-password">
|
||||||
|
{{ $t('can_reset_password') }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton type="link">
|
||||||
|
{{ $t('write_us') }}
|
||||||
|
</UiButton>
|
||||||
|
</i18n-t>
|
||||||
|
</UiAccordionItem>
|
||||||
|
|
||||||
|
<UiAccordionItem
|
||||||
|
:title="$t('forgot_password_questions.access_to_mail.title')"
|
||||||
|
>
|
||||||
|
<i18n-t
|
||||||
|
scope="global"
|
||||||
|
keypath="forgot_password_questions.access_to_mail.content"
|
||||||
|
tag="p"
|
||||||
|
>
|
||||||
|
<UiButton type="link">
|
||||||
|
{{ $t('fill_the_form') }}
|
||||||
|
</UiButton>
|
||||||
|
</i18n-t>
|
||||||
|
</UiAccordionItem>
|
||||||
|
|
||||||
|
<UiAccordionItem :title="$t('forgot_password_questions.no_mail.title')">
|
||||||
|
<i18n-t
|
||||||
|
keypath="forgot_password_questions.no_mail.content"
|
||||||
|
tag="p"
|
||||||
|
scope="global"
|
||||||
|
>
|
||||||
|
<UiButton type="link">
|
||||||
|
{{ $t('write_us') }}
|
||||||
|
</UiButton>
|
||||||
|
</i18n-t>
|
||||||
|
</UiAccordionItem>
|
||||||
|
</UiAccordion>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
131
apps/client/components/checkbox-button.vue
Normal file
131
apps/client/components/checkbox-button.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
cn.b(),
|
||||||
|
cn.is('checked', checked),
|
||||||
|
cn.is('invalid', invalid),
|
||||||
|
cn.is('disabled', disabled),
|
||||||
|
cn.is('focused', focused),
|
||||||
|
]"
|
||||||
|
@click="handleChange"
|
||||||
|
>
|
||||||
|
<UiIconSCheck :class="[cn.e('checkmark')]" />
|
||||||
|
|
||||||
|
<p :class="[cn.e('label')]">
|
||||||
|
<slot v-bind="{ checked }">
|
||||||
|
{{ label }}
|
||||||
|
</slot>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
export interface Props {
|
||||||
|
id: string
|
||||||
|
label?: string
|
||||||
|
disabled?: boolean
|
||||||
|
modelValue?: boolean | string | number
|
||||||
|
trueValue?: boolean | string | number
|
||||||
|
falseValue?: boolean | string | number
|
||||||
|
required?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'CheckboxButton',
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
disabled: false,
|
||||||
|
trueValue: true,
|
||||||
|
falseValue: false,
|
||||||
|
required: false,
|
||||||
|
modelValue: undefined,
|
||||||
|
})
|
||||||
|
const slots = useSlots()
|
||||||
|
const { checked, invalid, focused, handleChange } = useCheckbox(props, slots)
|
||||||
|
|
||||||
|
const cn = useClassname('checkbox-button')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.checkbox-button {
|
||||||
|
$self: &;
|
||||||
|
|
||||||
|
@include txt-i-m;
|
||||||
|
|
||||||
|
--border-color: #{$clr-grey-300};
|
||||||
|
--border-width: 1px;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 40px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
outline: var(--border-width) solid var(--border-color);
|
||||||
|
outline-offset: calc(var(--border-width) * -1);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
transition-property: outline-color, background-color, color;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--border-color: transparent;
|
||||||
|
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-checked {
|
||||||
|
--border-color: transparent;
|
||||||
|
|
||||||
|
background-color: $clr-cyan-500;
|
||||||
|
color: $clr-white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $clr-cyan-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//&.is-invalid {
|
||||||
|
// --border-color: var(--checkbox-invalid-border-color);
|
||||||
|
//}
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__checkmark {
|
||||||
|
--border-color: var(--checkbox-border-color);
|
||||||
|
--border-width: 1px;
|
||||||
|
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: var(--border-width) solid var(--border-color);
|
||||||
|
outline-offset: calc(var(--border-width) * -1);
|
||||||
|
color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
transition-property: outline-color, background-color, color;
|
||||||
|
margin-right: 8px;
|
||||||
|
|
||||||
|
#{$self}.has-label & {
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#{$self}.is-checked & {
|
||||||
|
--border-width: 0;
|
||||||
|
|
||||||
|
color: var(--checkbox-checked-color);
|
||||||
|
background-color: var(--checkbox-checked-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
#{$self}.is-disabled & {
|
||||||
|
--border-color: var(--checkbox-disabled-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#{$self}.is-disabled.is-checked & {
|
||||||
|
color: var(--checkbox-disabled-checked-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
73
apps/client/components/currency-name.vue
Normal file
73
apps/client/components/currency-name.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="currency-name"
|
||||||
|
:class="`currency-name--${size}`"
|
||||||
|
>
|
||||||
|
<UiCoin
|
||||||
|
class="currency-name__coin"
|
||||||
|
:code="code"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="size !== 'small'"
|
||||||
|
class="currency-name__name"
|
||||||
|
>
|
||||||
|
{{ name || code }}
|
||||||
|
</span>
|
||||||
|
<span class="currency-name__code">{{ code }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
export interface Props {
|
||||||
|
code: string
|
||||||
|
name?: string
|
||||||
|
size?: 'small' | 'medium'
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'CurrencyName',
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
size: 'medium',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.currency-name {
|
||||||
|
&--medium {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 32px auto;
|
||||||
|
gap: var(--currency-name-gap, 4px 8px);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--small {
|
||||||
|
--coin-size: 20px;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--currency-name-gap, 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__coin {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
@include txt-r-sb('currency-name');
|
||||||
|
|
||||||
|
color: $clr-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__code {
|
||||||
|
@include txt-s-m('currency-name-code');
|
||||||
|
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
apps/client/components/form-header.vue
Normal file
35
apps/client/components/form-header.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-header">
|
||||||
|
<UiButton
|
||||||
|
icon="chevron-left"
|
||||||
|
color="secondary"
|
||||||
|
type="link"
|
||||||
|
class="form-header__back"
|
||||||
|
:href="backLink"
|
||||||
|
>
|
||||||
|
{{ backText }}
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<slot name="title">
|
||||||
|
<h1 class="form-header__title">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
backLink: { type: String, required: true },
|
||||||
|
backText: { type: String, required: true },
|
||||||
|
title: { type: String, required: true },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.form-header {
|
||||||
|
&__back {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
44
apps/client/components/formatted-date.vue
Normal file
44
apps/client/components/formatted-date.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="formatted-date">
|
||||||
|
<p class="formatted-date__date">
|
||||||
|
{{ date }} <span class="formatted-date__time">{{ time }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="formatted-date__zone">
|
||||||
|
(UTC+3)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: { type: String, required: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const dayObj = computed(() => dayjs(props.value))
|
||||||
|
const date = computed(() => dayObj.value.format('DD.MM.YY'))
|
||||||
|
const time = computed(() => dayObj.value.format('HH:mm'))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.formatted-date {
|
||||||
|
&__date {
|
||||||
|
@include txt-r-sb;
|
||||||
|
|
||||||
|
color: $clr-black;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time {
|
||||||
|
color: $clr-grey-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__zone {
|
||||||
|
@include txt-s-m;
|
||||||
|
|
||||||
|
color: $clr-grey-500;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
46
apps/client/components/invoices/empty.vue
Normal file
46
apps/client/components/invoices/empty.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="invoices-empty">
|
||||||
|
<img
|
||||||
|
src="/flag.svg"
|
||||||
|
class="mb-40"
|
||||||
|
>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
{{ $t('your_invoices_will_be_displayed_here.title') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<h4 class="invoices-empty__text">
|
||||||
|
{{ $t('your_invoices_will_be_displayed_here.content') }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
size="large"
|
||||||
|
class="mt-40 w-100"
|
||||||
|
:href="`/create/invoice/${id}`"
|
||||||
|
>
|
||||||
|
{{ $t('create_an_invoice') }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({ id: { type: String, required: true } })
|
||||||
|
|
||||||
|
defineEmits(['create'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.invoices-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 422px;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: $clr-grey-400;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
79
apps/client/components/navigation/item.vue
Normal file
79
apps/client/components/navigation/item.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLink
|
||||||
|
class="nav-item"
|
||||||
|
:class="{
|
||||||
|
'nav-item--active': isActive,
|
||||||
|
}"
|
||||||
|
:to="to"
|
||||||
|
>
|
||||||
|
<slot name="icon">
|
||||||
|
<Component
|
||||||
|
:is="resolveComponent(`ui-icon-${icon}`)"
|
||||||
|
class="nav-item__icon"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
<span class="nav-item__title">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { UiIcon } from '#build/types/ui/icons'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
to: string
|
||||||
|
icon: UiIcon
|
||||||
|
title: string
|
||||||
|
matcher?: () => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const isActive = computed(() => (props.matcher ? props.matcher() : false))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.nav-item {
|
||||||
|
--link-color: #{$clr-white};
|
||||||
|
|
||||||
|
@include txt-i-sb;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--link-color);
|
||||||
|
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--link-color: #{$clr-grey-500};
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active,
|
||||||
|
&:active {
|
||||||
|
--link-color: #{$clr-cyan-300} !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__notification-icon {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
top: -8px;
|
||||||
|
right: -24px;
|
||||||
|
background: $clr-white;
|
||||||
|
color: $clr-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
10
apps/client/components/navigation/logo.vue
Normal file
10
apps/client/components/navigation/logo.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLink to="/projects">
|
||||||
|
<img
|
||||||
|
class="app-logo__img"
|
||||||
|
src="/logo-with-text.svg"
|
||||||
|
alt="logo"
|
||||||
|
draggable="false"
|
||||||
|
>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
46
apps/client/components/navigation/sidebar.vue
Normal file
46
apps/client/components/navigation/sidebar.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<nav class="nav-sidebar">
|
||||||
|
<div
|
||||||
|
v-if="$slots.logo"
|
||||||
|
class="nav-sidebar__logo"
|
||||||
|
>
|
||||||
|
<slot name="logo" />
|
||||||
|
</div>
|
||||||
|
<div class="nav-sidebar__content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div class="nav-sidebar__bottom">
|
||||||
|
<slot name="bottom" />
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.nav-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
background: #{$clr-black};
|
||||||
|
color: #{$clr-white};
|
||||||
|
padding: 32px 27px 32px 27px;
|
||||||
|
|
||||||
|
width: 120px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__logo {
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
gap: 20px;
|
||||||
|
margin-inline: -27px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
183
apps/client/components/network-select.vue
Normal file
183
apps/client/components/network-select.vue
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="network-select network-select--secondary network-select--large"
|
||||||
|
:class="{
|
||||||
|
'has-value': !!field.value.value,
|
||||||
|
'is-disabled': disabled,
|
||||||
|
'is-invalid': invalid,
|
||||||
|
}"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@click="show"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
ref="wrapper"
|
||||||
|
class="network-select__wrapper"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown.enter="show"
|
||||||
|
@keydown.space="show"
|
||||||
|
>
|
||||||
|
|
||||||
|
<span class="network-select__content">
|
||||||
|
<div
|
||||||
|
v-if="!!field.value.value"
|
||||||
|
class="network-select__value"
|
||||||
|
>
|
||||||
|
<UiCoin
|
||||||
|
:code="assetCode"
|
||||||
|
class="network-select__coin"
|
||||||
|
/>
|
||||||
|
<span>{{ modelValue }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="network-select__label">{{ $t('network') }}</p>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
class="network-select__action"
|
||||||
|
type="ghost"
|
||||||
|
size="small"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
{{ actionText }}
|
||||||
|
</UiButton>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="invalid"
|
||||||
|
class="network-select__bottom"
|
||||||
|
>
|
||||||
|
<div class="network-select__validation-message">
|
||||||
|
{{ field.errorMessage.value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, toRef, watch } from 'vue'
|
||||||
|
import { useField } from 'vee-validate'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
assetCode: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const id = toRef(props, 'id')
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const field = useField(id, 'required', {
|
||||||
|
validateOnValueUpdate: false,
|
||||||
|
syncVModel: true,
|
||||||
|
initialValue: !isEmptyValue(props.modelValue) ? props.modelValue : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapper = ref()
|
||||||
|
const active = ref(false)
|
||||||
|
|
||||||
|
const invalid = computed(() => !props.disabled && !!field.errorMessage.value)
|
||||||
|
|
||||||
|
const actionText = computed(() =>
|
||||||
|
field.value.value ? t('change') : t('select'),
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(field.value, hide)
|
||||||
|
|
||||||
|
watch(active, (value) => {
|
||||||
|
if (!value)
|
||||||
|
wrapper.value?.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
active.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
active.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEmptyValue(value) {
|
||||||
|
return [null, undefined, ''].includes(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.network-select {
|
||||||
|
$self: &;
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
display: block;
|
||||||
|
border-radius: 12px;
|
||||||
|
outline: 1px solid $clr-grey-300;
|
||||||
|
outline-offset: -1px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
background-color: $clr-white;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
@include txt-i-m;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 15px;
|
||||||
|
left: 0;
|
||||||
|
color: $clr-grey-400;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
|
||||||
|
#{$self}.has-value & {
|
||||||
|
transform: translateY(-7px) scale(0.78);
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
@include txt-i-m;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding-block: 22px 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__coin {
|
||||||
|
--coin-size: 16px;
|
||||||
|
--coin-border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bottom {
|
||||||
|
@include txt-s-m;
|
||||||
|
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__validation-message {
|
||||||
|
color: var(--input-validation-message-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
110
apps/client/components/notification-card/base.vue
Normal file
110
apps/client/components/notification-card/base.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="notification-card" :class="{ 'not-read': !read }">
|
||||||
|
<div class="notification-card__icon">
|
||||||
|
<Component :is="resolveComponent(`ui-icon-${icon}`)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notification-card__content">
|
||||||
|
<p class="notification-card__title">
|
||||||
|
{{ title }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="notification-card__subtitle-wrapper">
|
||||||
|
<span class="notification-card__subtitle">
|
||||||
|
{{ subtitle }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="notification-card__date">
|
||||||
|
Сегодня в 14:40
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { UiIcon } from '#build/types/ui/icons'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
icon: UiIcon
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
read: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.notification-card {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 32px auto;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: 1px solid transparent;
|
||||||
|
outline-offset: -1px;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
transition-property: background-color, outline-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #F7F9FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
outline-color: $clr-cyan-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.not-read {
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 8px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: $clr-red-500;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--notification-card-icon-color, $clr-grey-500);
|
||||||
|
background-color: var(--notification-card-icon-background, $clr-grey-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
@include txt-r-sb;
|
||||||
|
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle-wrapper {
|
||||||
|
@include txt-t-m;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
color: $clr-grey-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date {
|
||||||
|
color: $clr-grey-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<NotificationCardBase icon="s-up-right" v-bind="props" class="card">
|
||||||
|
<p class="amount">
|
||||||
|
<span>Валюта: <strong>USDT</strong></span>
|
||||||
|
<span>Сумма: <strong>500</strong></span>
|
||||||
|
</p>
|
||||||
|
</NotificationCardBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Props } from './base.vue'
|
||||||
|
|
||||||
|
const props = defineProps<Omit<Props, 'icon'>>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card {
|
||||||
|
--notification-card-icon-color: #{$clr-cyan-600};
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
@include txt-s;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
margin-top: 8px;
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
39
apps/client/components/notification-card/deposit.vue
Normal file
39
apps/client/components/notification-card/deposit.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<NotificationCardBase icon="s-down-left" v-bind="props" class="card">
|
||||||
|
<p class="amount">
|
||||||
|
<span>Валюта: <strong>USDT</strong></span>
|
||||||
|
<span>Сумма: <strong>500</strong></span>
|
||||||
|
</p>
|
||||||
|
</NotificationCardBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Props } from './base.vue'
|
||||||
|
|
||||||
|
const props = defineProps<Omit<Props, 'icon'>>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card {
|
||||||
|
--notification-card-icon-color: #{$clr-green-500};
|
||||||
|
--notification-card-icon-background: #{$clr-green-100};
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
@include txt-s;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
margin-top: 8px;
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
43
apps/client/components/notification-card/sign-in.vue
Normal file
43
apps/client/components/notification-card/sign-in.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<NotificationCardBase icon="s-exit" v-bind="props">
|
||||||
|
<p class="location">
|
||||||
|
Местоположение: Moscow, Russia
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="alert">
|
||||||
|
Если это были не вы срочно
|
||||||
|
<UiButton type="link" size="small">
|
||||||
|
смените пароль
|
||||||
|
</UiButton>
|
||||||
|
или
|
||||||
|
<UiButton type="link" size="small">
|
||||||
|
свяжитесь с поддержкой
|
||||||
|
</UiButton>
|
||||||
|
</p>
|
||||||
|
</NotificationCardBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Props } from './base.vue'
|
||||||
|
|
||||||
|
const props = defineProps<Omit<Props, 'icon'>>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.location {
|
||||||
|
@include txt-s-m;
|
||||||
|
|
||||||
|
color: $clr-grey-400;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
@include txt-r-m;
|
||||||
|
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<NotificationCardBase icon="s-clock" v-bind="props" class="card">
|
||||||
|
<div class="row">
|
||||||
|
<p class="amount">
|
||||||
|
<span>Валюта: <strong>USDT</strong></span>
|
||||||
|
<span>Сумма: <strong>500</strong></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<UiButton class="support" size="small" type="outlined" color="secondary">
|
||||||
|
Support
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</NotificationCardBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Props } from './base.vue'
|
||||||
|
|
||||||
|
const props = defineProps<Omit<Props, 'icon'>>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card {
|
||||||
|
--notification-card-icon-color: #{$clr-warn-500};
|
||||||
|
--notification-card-icon-background: #{$clr-warn-200};
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
@include txt-s;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
107
apps/client/components/notifications-dropdown.vue
Normal file
107
apps/client/components/notifications-dropdown.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<UiDropdown
|
||||||
|
class="notifications-dropdown"
|
||||||
|
dropdown-class="notifications-dropdown__content"
|
||||||
|
placement="bottom-end"
|
||||||
|
trigger="hover"
|
||||||
|
:offset="12"
|
||||||
|
teleport
|
||||||
|
>
|
||||||
|
<template #default="{ isActive }">
|
||||||
|
<NotifyButton
|
||||||
|
icon="bell"
|
||||||
|
:count="0"
|
||||||
|
class="notifications-dropdown__trigger"
|
||||||
|
:state="isActive ? 'hover' : undefined"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #dropdown>
|
||||||
|
<div class="notifications-dropdown__header">
|
||||||
|
<h3>Notifications</h3>
|
||||||
|
|
||||||
|
<UiButton class="ml-a" size="small" icon="s-check-seen" type="ghost">
|
||||||
|
Mark all as read
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<UiButton class="ml-4" icon="s-kebab-android" type="link" size="small" color="secondary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="notifications-dropdown__filter">
|
||||||
|
<UiSwitcher
|
||||||
|
id="notification_type"
|
||||||
|
:options="[
|
||||||
|
{ label: 'All', value: 'all' },
|
||||||
|
{ label: 'Поступления', value: 'p' },
|
||||||
|
{ label: 'Списания', value: 's' },
|
||||||
|
]"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="notifications-dropdown__list">
|
||||||
|
<NotificationCardWithdrawCreated title="Новый вывод средств создан" subtitle="Адрес: 7u5RQ9g....bkNfG" />
|
||||||
|
<NotificationCardDeposit title="На ваш кошелек поступил новый платеж" subtitle="Адрес: 7u5RQ9g....bkNfG" />
|
||||||
|
<NotificationCardCompletedWithdraw title="Вывод средств был проведен успешно" subtitle="Адрес: 7u5RQ9g....bkNfG" />
|
||||||
|
<NotificationCardSignIn title="Вход в учетную запись" subtitle="IP: 185.218.108.156" read />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UiDropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
|
||||||
|
useForm({
|
||||||
|
keepValuesOnUnmount: true,
|
||||||
|
initialValues: {
|
||||||
|
notification_type: 'all',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.notifications-dropdown {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__filter {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
//margin-inline: -16px;
|
||||||
|
//padding-inline: 16px;
|
||||||
|
//max-height: 150px;
|
||||||
|
//overflow: auto;
|
||||||
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
position: relative;
|
||||||
|
width: 406px;
|
||||||
|
padding: 16px !important;
|
||||||
|
box-shadow: 0px 4px 4px 0px #6C86AD40;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: 12px;
|
||||||
|
background-color: $clr-white;
|
||||||
|
border-radius: 2px;
|
||||||
|
transform: rotateZ(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
57
apps/client/components/notify-button.vue
Normal file
57
apps/client/components/notify-button.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<UiButton
|
||||||
|
:class="[cn.b(), cn.has('one', count === 1), cn.has('few', count > 1)]"
|
||||||
|
:icon="icon"
|
||||||
|
type="ghost"
|
||||||
|
color="secondary"
|
||||||
|
:data-count="countContent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({ icon: { type: String }, count: { type: Number } })
|
||||||
|
|
||||||
|
const cn = useClassname('notify-button')
|
||||||
|
|
||||||
|
const countContent = computed(() => {
|
||||||
|
return props.count > 9 ? '9+' : props.count
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.notify-button {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@include txt-s-m;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
background-color: $clr-red-500;
|
||||||
|
color: $clr-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-one {
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
top: 7px;
|
||||||
|
right: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-few {
|
||||||
|
&::after {
|
||||||
|
content: attr(data-count);
|
||||||
|
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
padding-inline: 4px;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
21
apps/client/components/operation-type.vue
Normal file
21
apps/client/components/operation-type.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div class="operation-type">
|
||||||
|
<span class="mr-4">{{ type }}</span>
|
||||||
|
<UiIconSUpRight class="text-clr-cyan-500" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.operation-type{
|
||||||
|
@include txt-r-sb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
77
apps/client/components/page-form.vue
Normal file
77
apps/client/components/page-form.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<form
|
||||||
|
class="page-form"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<UiButton
|
||||||
|
icon="chevron-left"
|
||||||
|
color="secondary"
|
||||||
|
type="link"
|
||||||
|
class="mb-8"
|
||||||
|
:href="backLink"
|
||||||
|
>
|
||||||
|
{{ backText }}
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<slot name="title">
|
||||||
|
<h1 :class="titleOffset ? 'mb-32' : ''">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
</slot>
|
||||||
|
|
||||||
|
<div class="page-form__summary">
|
||||||
|
<StaticError class="mb-16" />
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot name="submit" v-bind="{ isSubmitting }">
|
||||||
|
<UiButton
|
||||||
|
class="page-form__submit"
|
||||||
|
size="large"
|
||||||
|
native-type="submit"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
>
|
||||||
|
{{ submitText }}
|
||||||
|
</UiButton>
|
||||||
|
</slot>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
backLink: { type: String, required: true },
|
||||||
|
backText: { type: String, required: true },
|
||||||
|
submitText: { type: String, required: true },
|
||||||
|
titleOffset: { type: Boolean, default: true },
|
||||||
|
handler: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { handleSubmit, isSubmitting } = useForm()
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (values) => {
|
||||||
|
await props.handler(values)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.page-form {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
&__summary {
|
||||||
|
padding-top: var(--page-form-padding-top, 24px);
|
||||||
|
padding-bottom: var(--page-form-padding-bottom, 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
51
apps/client/components/page/block.vue
Normal file
51
apps/client/components/page/block.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-block">
|
||||||
|
<div class="page-block__header">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="d-flex align-items-center" style="gap: 16px">
|
||||||
|
<h3 v-if="title">
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<slot name="badge" />
|
||||||
|
</div>
|
||||||
|
<span v-if="subTitle" class="page-block__subtitle">{{ subTitle }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: { type: String },
|
||||||
|
subTitle: { type: String },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.page-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--page-block-gap, 8px);
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: var(--page-block-padding, 16px);
|
||||||
|
background-color: $clr-white;
|
||||||
|
|
||||||
|
&__header{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle{
|
||||||
|
@include txt-i-m;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
78
apps/client/components/page/footer-info-block.vue
Normal file
78
apps/client/components/page/footer-info-block.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<div class="footer-info-block">
|
||||||
|
<div class="footer-info-block__left">
|
||||||
|
<UiIconAskForDiscountFilled class="footer-info-block__icon" />
|
||||||
|
<div>
|
||||||
|
<h3 class="footer-info-block__title">
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
<p class="footer-info-block__text">
|
||||||
|
{{ text }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-info-block__right">
|
||||||
|
<UiButton
|
||||||
|
color="secondary"
|
||||||
|
right-icon="s-chevron-right"
|
||||||
|
>
|
||||||
|
{{ action }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
text: { type: String, required: true },
|
||||||
|
action: { type: String, required: true },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.footer-info-block {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
color: $clr-grey-600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: $clr-grey-500;
|
||||||
|
width: 512px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
color: $clr-market-500 !important;
|
||||||
|
margin-right: 32px;
|
||||||
|
padding: 9px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__left {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
border-bottom-left-radius: 12px;
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
padding: 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
border-bottom-right-radius: 12px;
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
background-color: $clr-grey-300;
|
||||||
|
|
||||||
|
width: 286px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
90
apps/client/components/page/header.vue
Normal file
90
apps/client/components/page/header.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<header class="page-header">
|
||||||
|
<div class="page-header__title">
|
||||||
|
<UiButton
|
||||||
|
v-if="withBackButton"
|
||||||
|
icon="arrow-left"
|
||||||
|
color="secondary"
|
||||||
|
class="mr-24"
|
||||||
|
href="/projects"
|
||||||
|
type="outlined"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="$slots.default"
|
||||||
|
class="page-header__default"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-header__right">
|
||||||
|
<slot name="right">
|
||||||
|
<div class="d-flex align-items-center" style="gap: 24px;">
|
||||||
|
<NotificationsDropdown />
|
||||||
|
<ProfileDropdown />
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
withBackButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const notify = useNotify()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use 'sass:color';
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
position: sticky;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
background-size: 4px 4px;
|
||||||
|
background-image: radial-gradient(
|
||||||
|
color.change($clr-grey-100, $alpha: 0.7) 2px,
|
||||||
|
$clr-cyan-200
|
||||||
|
);
|
||||||
|
border-bottom: 1px solid #f4f6ff;
|
||||||
|
margin-inline: -16px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
top: 0;
|
||||||
|
z-index: 6000;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
//margin-bottom: 32px;
|
||||||
|
padding-block: 32px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__default {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
97
apps/client/components/page/info-block.vue
Normal file
97
apps/client/components/page/info-block.vue
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<div class="info-block">
|
||||||
|
<div class="d-flex justify-content-between mb-16">
|
||||||
|
<p class="info-block__title">
|
||||||
|
{{ title }}
|
||||||
|
</p>
|
||||||
|
<UiBadge v-if="badge" type="marketing">
|
||||||
|
{{ badge }}
|
||||||
|
</UiBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot name="info" />
|
||||||
|
|
||||||
|
<span class="info-block__text">{{ text }}</span>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
class="info-block__action"
|
||||||
|
type="outlined"
|
||||||
|
:href="link"
|
||||||
|
right-icon="s-chevron-right"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ action }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.info-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
width: 270px;
|
||||||
|
min-height: 179px;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
background-color: $clr-grey-100;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
@include txt-m-sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
@include txt-s;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action {
|
||||||
|
margin-top: auto;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-block-addInfo {
|
||||||
|
@include txt-s;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sum {
|
||||||
|
@include txt-m-sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
@include txt-r;
|
||||||
|
color: $clr-grey-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
apps/client/components/page/toolbar.vue
Normal file
32
apps/client/components/page/toolbar.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-toolbar">
|
||||||
|
<slot name="prefix" />
|
||||||
|
|
||||||
|
<UiSearch
|
||||||
|
class="flex-1"
|
||||||
|
size="large"
|
||||||
|
:label="searchLabel"
|
||||||
|
:model-value="search"
|
||||||
|
@update:model-value="$emit('update:search', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
searchLabel: { type: String, required: true },
|
||||||
|
search: { type: String },
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(['update:search'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.page-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
92
apps/client/components/profile-dropdown.vue
Normal file
92
apps/client/components/profile-dropdown.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<UiDropdown
|
||||||
|
v-if="authenticated"
|
||||||
|
class="profile-dropdown"
|
||||||
|
dropdown-class="profile-dropdown__content"
|
||||||
|
placement="bottom-end"
|
||||||
|
trigger="hover"
|
||||||
|
:offset="12"
|
||||||
|
teleport
|
||||||
|
>
|
||||||
|
<template #default="{ isActive }">
|
||||||
|
<UiButton class="profile-dropdown__trigger" icon="circle-userpic" :state="isActive ? 'hover' : undefined" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #dropdown>
|
||||||
|
<UiDropdownItem class="mb-12" @click="navigateTo('/2fa')">
|
||||||
|
<template #icon>
|
||||||
|
<UiIconSProtection class="text-clr-cyan-400" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<span>Безопасность</span>
|
||||||
|
</UiDropdownItem>
|
||||||
|
|
||||||
|
<UiDropdownItem class="mb-12" @click="navigateTo('/verification/status')">
|
||||||
|
<template #icon>
|
||||||
|
<UiIconSOverview class="text-clr-cyan-400" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<span>Лимиты</span>
|
||||||
|
</UiDropdownItem>
|
||||||
|
|
||||||
|
<UiDropdownItem @click="navigateTo('/settings')">
|
||||||
|
<template #icon>
|
||||||
|
<UiIconSSettings class="text-clr-cyan-400" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<span>Настройки</span>
|
||||||
|
</UiDropdownItem>
|
||||||
|
|
||||||
|
<UiDropdownSeparator />
|
||||||
|
|
||||||
|
<UiDropdownItem @click="logout">
|
||||||
|
<template #icon>
|
||||||
|
<UiIconSExit class="text-clr-grey-400" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<span>Logout</span>
|
||||||
|
</UiDropdownItem>
|
||||||
|
</template>
|
||||||
|
</UiDropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const { logout, authenticated } = useAuth()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.profile-dropdown {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&__trigger {
|
||||||
|
--button-color: #{$clr-grey-500};
|
||||||
|
--button-background: #{$clr-grey-200};
|
||||||
|
--button-hover-color: #{$clr-white};
|
||||||
|
--button-active-color: #{$clr-white};
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
@include txt-s-m('dropdown-item', true);
|
||||||
|
|
||||||
|
--dropdown-item-padding: 8px 12px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
color: $clr-grey-600;
|
||||||
|
width: 233px;
|
||||||
|
padding: 16px !important;
|
||||||
|
box-shadow: 0px 4px 4px 0px #6C86AD40;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
transform: rotateZ(45deg);
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: 12px;
|
||||||
|
background-color: $clr-white;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
apps/client/components/project/create.vue
Normal file
35
apps/client/components/project/create.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="project-create">
|
||||||
|
<img src="/flag.svg" class="mb-40">
|
||||||
|
|
||||||
|
<i18n-t scope="global" keypath="create_your_project.title" tag="h2">
|
||||||
|
<span class="text-clr-cyan-500">
|
||||||
|
{{ $t('create_your_project.create') }}
|
||||||
|
</span>
|
||||||
|
</i18n-t>
|
||||||
|
|
||||||
|
<h4 class="project-create__text">
|
||||||
|
{{ $t('create_your_project.content') }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<UiButton size="large" class="mt-40 w-100" href="/projects/create">
|
||||||
|
{{ $t('create') }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.project-create {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 422px;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: $clr-grey-400;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
104
apps/client/components/project/info-columns.vue
Normal file
104
apps/client/components/project/info-columns.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="info-columns">
|
||||||
|
<PageInfoBlock
|
||||||
|
title="More limits"
|
||||||
|
text="Get increased limits and advanced features by providing a little more profile information"
|
||||||
|
action="Increase the limit"
|
||||||
|
link="/verification/status"
|
||||||
|
badge="BASIC"
|
||||||
|
style="
|
||||||
|
background-image: url('/block2.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: bottom right;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #info>
|
||||||
|
<div
|
||||||
|
v-if="!maxState"
|
||||||
|
class="limits"
|
||||||
|
>
|
||||||
|
<p class="limits__date">
|
||||||
|
Updated on <strong>{{ dayjs().format('DD.MM.YYYY') }}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="limits__title">
|
||||||
|
Remaining limit
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mb-4 d-flex justify-content-between">
|
||||||
|
<span class="limits__current">{{ dayLimit }} <span class="limits__current-text">/ per day</span></span>
|
||||||
|
|
||||||
|
<span class="limits__total">{{ curDayLimit }} USDT</span>
|
||||||
|
</div>
|
||||||
|
<UiProgressBar :progress="(curDayLimit / dayLimit) * 100" />
|
||||||
|
|
||||||
|
<div class="mb-4 mt-24 d-flex justify-content-between">
|
||||||
|
<span class="limits__current">{{ monthLimit }} <span class="limits__current-text">/ per month</span></span>
|
||||||
|
|
||||||
|
<span class="limits__total">{{ curMonthLimit }} USDT</span>
|
||||||
|
</div>
|
||||||
|
<UiProgressBar :progress="(curMonthLimit / monthLimit) * 100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PageInfoBlock>
|
||||||
|
|
||||||
|
<PageInfoBlock
|
||||||
|
title="2FA authentication"
|
||||||
|
text="Complete the verification process to remove withdrawal limits."
|
||||||
|
action="Add 2FA"
|
||||||
|
link="/2fa"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const maxState = ref(false)
|
||||||
|
|
||||||
|
const curDayLimit = ref(100)
|
||||||
|
const curMonthLimit = ref(5200)
|
||||||
|
|
||||||
|
const dayLimit = ref(1000)
|
||||||
|
const monthLimit = ref(10000)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.info-columns {
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.limits {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
&__date {
|
||||||
|
@include txt-s;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
@include txt-s-m;
|
||||||
|
color: $clr-grey-400;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__total {
|
||||||
|
@include txt-s-m;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__current {
|
||||||
|
@include txt-m-sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__current-text{
|
||||||
|
@include txt-r-sb;
|
||||||
|
color: $clr-grey-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
162
apps/client/components/projects-table.vue
Normal file
162
apps/client/components/projects-table.vue
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<UiPlainTable class="projects-table" :columns="columns" :data="projects">
|
||||||
|
<template #cell(name)="{ row }">
|
||||||
|
<div class="name-cell">
|
||||||
|
<div class="name-cell__initials">
|
||||||
|
{{ getProjectInitials(row.original) }}
|
||||||
|
</div>
|
||||||
|
<div class="name-cell__name">
|
||||||
|
{{ row.original.name }}
|
||||||
|
</div>
|
||||||
|
<div class="name-cell__id">
|
||||||
|
id {{ row.original.id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(balance)="{ row }">
|
||||||
|
<MoneyAmount :value="row.original.account.balance" currency="USDT" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(withdraw)="{ row }">
|
||||||
|
<MoneyAmount value="0" currency="USDT" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(actions)="{ row }">
|
||||||
|
<UiButton class="mr-16" color="secondary">
|
||||||
|
API
|
||||||
|
</UiButton>
|
||||||
|
<UiButton class="mr-16" color="secondary">
|
||||||
|
Settings
|
||||||
|
</UiButton>
|
||||||
|
<UiButton :href="`/projects/${row.original.id}`">
|
||||||
|
Open
|
||||||
|
</UiButton>
|
||||||
|
</template>
|
||||||
|
</UiPlainTable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { createColumnHelper } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
interface ProjectListItem {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
projects: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<ProjectListItem>()
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor('name', {
|
||||||
|
header: 'Name',
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'balance',
|
||||||
|
header: 'Balance',
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'withdraw',
|
||||||
|
header: 'In sending',
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'actions',
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
function getProjectInitials(project: ProjectListItem) {
|
||||||
|
return project.name.slice(0, 1)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.projects-table {
|
||||||
|
@include txt-i-sb('money-amount-value', true);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0 8px;
|
||||||
|
text-align: left;
|
||||||
|
margin-block: -8px;
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
margin: 0;
|
||||||
|
padding-inline: 16px;
|
||||||
|
background-color: $clr-grey-100;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
border-bottom-left-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
border-bottom-right-radius: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
@include txt-i-m;
|
||||||
|
|
||||||
|
color: $clr-grey-500;
|
||||||
|
padding-block: 16px;
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
padding-left: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding-block: 24px;
|
||||||
|
height: 95.8px;
|
||||||
|
|
||||||
|
&.actions {
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
padding-left: 32px;
|
||||||
|
width: 1%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-cell {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: 32px auto;
|
||||||
|
gap: 4px 16px;
|
||||||
|
|
||||||
|
&__initials {
|
||||||
|
@include font(20px, 500, 32px);
|
||||||
|
|
||||||
|
background-color: $clr-grey-400;
|
||||||
|
height: 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: $clr-white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
@include txt-i-sb;
|
||||||
|
|
||||||
|
grid-column: 2;
|
||||||
|
color: $clr-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__id {
|
||||||
|
@include txt-s-m;
|
||||||
|
|
||||||
|
grid-column: 2;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
106
apps/client/components/radio-button.vue
Normal file
106
apps/client/components/radio-button.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
cn.b(),
|
||||||
|
cn.is('checked', checked),
|
||||||
|
cn.is('disabled', disabled),
|
||||||
|
cn.is('focused', focused),
|
||||||
|
cn.is('disabled', disabled),
|
||||||
|
]"
|
||||||
|
@click="handleChange"
|
||||||
|
>
|
||||||
|
<p :class="[cn.e('label')]">
|
||||||
|
<slot>
|
||||||
|
{{ label }}
|
||||||
|
</slot>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-if="caption || $slots.caption" :class="[cn.e('caption')]">
|
||||||
|
<slot name="caption">
|
||||||
|
{{ caption }}
|
||||||
|
</slot>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
export interface Props {
|
||||||
|
id: string
|
||||||
|
value: string | number
|
||||||
|
label?: string
|
||||||
|
caption?: string
|
||||||
|
disabled?: boolean
|
||||||
|
modelValue?: string | number
|
||||||
|
required?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'RadioButton',
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
disabled: false,
|
||||||
|
trueValue: true,
|
||||||
|
falseValue: false,
|
||||||
|
required: false,
|
||||||
|
modelValue: undefined,
|
||||||
|
})
|
||||||
|
const slots = useSlots()
|
||||||
|
const { checked, focused, handleChange } = useRadio(props, slots)
|
||||||
|
|
||||||
|
const cn = useClassname('radio-button')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.radio-button {
|
||||||
|
$self: &;
|
||||||
|
|
||||||
|
@include txt-i-m;
|
||||||
|
|
||||||
|
--border-color: #{$clr-grey-300};
|
||||||
|
--border-width: 1px;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
height: 48px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
outline: var(--border-width) solid var(--border-color);
|
||||||
|
outline-offset: calc(var(--border-width) * -1);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: .2s ease-out;
|
||||||
|
transition-property: outline-color, background-color, color;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--border-color: transparent;
|
||||||
|
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-checked {
|
||||||
|
--border-color: transparent;
|
||||||
|
|
||||||
|
background-color: $clr-cyan-500;
|
||||||
|
color: $clr-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__caption {
|
||||||
|
@include txt-r-m;
|
||||||
|
|
||||||
|
color: $clr-cyan-400;
|
||||||
|
|
||||||
|
#{$self}.is-checked & {
|
||||||
|
color: $clr-cyan-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
145
apps/client/components/resource-filter/base.vue
Normal file
145
apps/client/components/resource-filter/base.vue
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<template>
|
||||||
|
<UiDropdown
|
||||||
|
class="resource-filter"
|
||||||
|
position="bottom-start"
|
||||||
|
:offset="0"
|
||||||
|
dropdown-class="resource-filter__dropdown"
|
||||||
|
transition-name="ui-select"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div class="resource-filter__wrapper">
|
||||||
|
<p class="resource-filter__label">
|
||||||
|
{{ label }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="resource-filter__content">
|
||||||
|
<p class="resource-filter__value">
|
||||||
|
{{ value }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<i
|
||||||
|
v-if="filled"
|
||||||
|
class="resource-filter__clear icon-s-cross-compact"
|
||||||
|
tabindex="0"
|
||||||
|
@click.stop="$emit('clear')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiIconChevronDown class="resource-filter__chevron" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #dropdown>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
</UiDropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
export interface Props {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
filled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
filled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(['clear'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.resource-filter {
|
||||||
|
$self: &;
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: 'label chevron' 'content chevron';
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
column-gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--select-background);
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
transition-property: background-color, border-radius, box-shadow;
|
||||||
|
outline: none;
|
||||||
|
width: 169px;
|
||||||
|
|
||||||
|
&:focus-visible,
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--select-hover-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--select-active-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
#{$self}.is-active & {
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
box-shadow: 0 4px 4px 0 #6c86ad40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
@include txt-s-m;
|
||||||
|
|
||||||
|
grid-area: label;
|
||||||
|
color: var(--select-label-color);
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
#{$self}.is-active &,
|
||||||
|
#{$self}__wrapper:hover &,
|
||||||
|
#{$self}__wrapper:focus-visible & {
|
||||||
|
color: var(--select-label-hover-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
grid-area: content;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
@include txt-i-m;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
color: var(--select-color);
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
appearance: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--select-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__clear {
|
||||||
|
color: var(--select-clear-color);
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--select-clear-hover-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__chevron {
|
||||||
|
color: var(--select-chevron-color);
|
||||||
|
grid-area: chevron;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dropdown {
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
box-shadow: 0 4px 4px 0 #6c86ad40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
51
apps/client/components/resource-filter/calendar.vue
Normal file
51
apps/client/components/resource-filter/calendar.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<ResourceFilterBase
|
||||||
|
:label="label"
|
||||||
|
:value="value"
|
||||||
|
:filled="modelValue && modelValue.length > 0"
|
||||||
|
@clear="$emit('update:modelValue', [])"
|
||||||
|
>
|
||||||
|
<UiCalendar
|
||||||
|
:model-value="modelValue"
|
||||||
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
|
/>
|
||||||
|
</ResourceFilterBase>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
label: string
|
||||||
|
placeholder: string
|
||||||
|
modelValue?: [from?: number, to?: number]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ResourceFilterCalendar',
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const value = computed(() => {
|
||||||
|
if (!props.modelValue)
|
||||||
|
return props.placeholder
|
||||||
|
|
||||||
|
const [start, end] = props.modelValue
|
||||||
|
|
||||||
|
if (!start)
|
||||||
|
return props.placeholder
|
||||||
|
|
||||||
|
if (start && end) {
|
||||||
|
const from = dayjs(start).format('DD.MM')
|
||||||
|
const to = dayjs(end).format('DD.MM.YY')
|
||||||
|
|
||||||
|
return `${from}–${to}`
|
||||||
|
}
|
||||||
|
else if (start) {
|
||||||
|
return dayjs(start).format('DD.MM.YY')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
23
apps/client/components/resource-filter/select.vue
Normal file
23
apps/client/components/resource-filter/select.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<UiSelect
|
||||||
|
class="resource-filter resource-filter--select"
|
||||||
|
v-bind="props"
|
||||||
|
:model-value="modelValue"
|
||||||
|
clearable
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Props } from 'ui-layer/components/select/types'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ResourceFilterSelect',
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
defineEmits(['update:modelValue'])
|
||||||
|
</script>
|
||||||
68
apps/client/components/resource-filters.vue
Normal file
68
apps/client/components/resource-filters.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="resource-filters">
|
||||||
|
<div class="resource-filters__filters">
|
||||||
|
<Component
|
||||||
|
:is="getFilterComponent(filter)"
|
||||||
|
v-for="filter in schema as Filters"
|
||||||
|
:id="`resource_filter.${filter.key}`"
|
||||||
|
v-bind="filter"
|
||||||
|
:key="filter.key"
|
||||||
|
:model-value="appliedFiltersRaw[filter.key]"
|
||||||
|
@update:model-value="apply(filter.key, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
v-if="!empty"
|
||||||
|
type="outlined"
|
||||||
|
icon="s-cross"
|
||||||
|
class="resource-filters__clear"
|
||||||
|
@click="reset"
|
||||||
|
>
|
||||||
|
Clear filters
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Filter, Filters } from '#imports'
|
||||||
|
|
||||||
|
const { schema, appliedFiltersRaw, empty, apply, reset } = inject(
|
||||||
|
filtersContextKey,
|
||||||
|
{
|
||||||
|
schema: [],
|
||||||
|
appliedFiltersRaw: {},
|
||||||
|
appliedFilters: computed(() => ({})),
|
||||||
|
empty: true,
|
||||||
|
apply: () => {},
|
||||||
|
reset: () => {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function getFilterComponent(filter: Filter) {
|
||||||
|
switch (filter.type) {
|
||||||
|
case 'calendar':
|
||||||
|
return resolveComponent('ResourceFilterCalendar')
|
||||||
|
case 'select':
|
||||||
|
default:
|
||||||
|
return resolveComponent('ResourceFilterSelect')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.resource-filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
&__filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__clear {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<SettingsRawTable :columns="columns" :data="data">
|
||||||
|
<template #cell(finance_notifications)="{ row }">
|
||||||
|
<div class="d-flex align-items-center text-clr-grey-600">
|
||||||
|
<Component :is="resolveComponent(`ui-icon-${row.original.icon}`)" :style="`color: ${row.original.color}`" />
|
||||||
|
<h4 class="ml-10">
|
||||||
|
{{ row.original.finance_notifications }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(telegram)="{ row }">
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<UiCheckbox id="telegram" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(mail)="{ row }">
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<UiCheckbox id="mail" :model-value="true" disabled />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(push)="{ row }">
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<UiCheckbox id="push" :model-value="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SettingsRawTable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { createColumnHelper } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper()
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'finance_notifications',
|
||||||
|
header: 'Finance Notifications',
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'telegram',
|
||||||
|
header: 'Telegram',
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'mail',
|
||||||
|
header: 'Mail',
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'push',
|
||||||
|
header: 'Push',
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{ finance_notifications: 'Поступления средств', icon: 'ArrowReceive', color: '#10C44C' },
|
||||||
|
{ finance_notifications: 'Выводы', icon: 'ArrowSend', color: '#1464D3' },
|
||||||
|
{ finance_notifications: 'Счет частично оплачен', icon: 'InstallmentPlan' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
45
apps/client/components/settings/limit-progress.vue
Normal file
45
apps/client/components/settings/limit-progress.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="block">
|
||||||
|
<span class="text-small">Spent</span>
|
||||||
|
<span class="text-small">Remaining limit </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiProgressBar class="progress" :progress="(amount / maxAmount) * 100" />
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<span class="text-amount">{{ amount }} <span class="text-currency">{{ currency }}</span></span>
|
||||||
|
<span class="text-amount">{{ maxAmount - amount }} <span class="text-currency">{{ currency }}</span> </span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
currency: { type: String, required: true },
|
||||||
|
amount: { type: Number, required: true },
|
||||||
|
maxAmount: { type: Number, required: true },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.block{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress{
|
||||||
|
margin-block: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-small {
|
||||||
|
@include txt-r;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-currency {
|
||||||
|
@include txt-i-sb;
|
||||||
|
color: $clr-grey-400;
|
||||||
|
}
|
||||||
|
.text-amount{
|
||||||
|
@include txt-i-sb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
apps/client/components/settings/property-item.vue
Normal file
50
apps/client/components/settings/property-item.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="settings-property-item">
|
||||||
|
<Component
|
||||||
|
:is="resolveComponent(`ui-icon-${icon}`)"
|
||||||
|
class="settings-property-item__icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="settings-property-item-content">
|
||||||
|
<p class="settings-property-item-content__title">
|
||||||
|
{{ title }}
|
||||||
|
</p>
|
||||||
|
<p v-if="text" class="settings-property-item-content__text">
|
||||||
|
{{ text }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
icon: { type: String, required: true },
|
||||||
|
title: { type: String, required: true },
|
||||||
|
text: { type: String },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.settings-property-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__icon{
|
||||||
|
color: $clr-cyan-500;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-property-item-content{
|
||||||
|
&__title {
|
||||||
|
@include txt-i-sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
@include txt-s-m;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
apps/client/components/settings/property.vue
Normal file
24
apps/client/components/settings/property.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div class="settings-property">
|
||||||
|
<SettingsPropertyItem :icon="icon" :text="text" :title="title" />
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
icon: { type: String, required: true },
|
||||||
|
title: { type: String, required: true },
|
||||||
|
text: { type: String },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.settings-property {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
height: 62px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
112
apps/client/components/settings/raw-table.vue
Normal file
112
apps/client/components/settings/raw-table.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<table class="settings-table">
|
||||||
|
<thead>
|
||||||
|
<tr v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
||||||
|
<th
|
||||||
|
v-for="header in headerGroup.headers"
|
||||||
|
:key="header.id"
|
||||||
|
:colSpan="header.colSpan"
|
||||||
|
:class="[header.id]"
|
||||||
|
:style="{
|
||||||
|
width: `${header.getSize()}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template v-if="!header.isPlaceholder">
|
||||||
|
<FlexRender
|
||||||
|
:render="header.column.columnDef.header"
|
||||||
|
:props="header.getContext()"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<slot>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="row in table.getRowModel().rows" :key="row.id">
|
||||||
|
<td
|
||||||
|
v-for="cell in row.getVisibleCells()"
|
||||||
|
:key="cell.id"
|
||||||
|
:class="[cell.column.id]"
|
||||||
|
>
|
||||||
|
<slot :name="`cell(${cell.column.id})`" v-bind="cell.getContext()">
|
||||||
|
<FlexRender
|
||||||
|
:render="cell.column.columnDef.cell"
|
||||||
|
:props="cell.getContext()"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</slot>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FlexRender, getCoreRowModel, useVueTable } from '@tanstack/vue-table'
|
||||||
|
import type { ColumnDef } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
columns: ColumnDef<unknown>[]
|
||||||
|
data: unknown[]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SettingsTable',
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const table = useVueTable({
|
||||||
|
get data() {
|
||||||
|
return props.data
|
||||||
|
},
|
||||||
|
get columns() {
|
||||||
|
return props.columns
|
||||||
|
},
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.settings-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0 8px;
|
||||||
|
|
||||||
|
margin-block: -8px;
|
||||||
|
|
||||||
|
th {
|
||||||
|
@include h5;
|
||||||
|
|
||||||
|
background-color: $clr-grey-100;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
border-bottom-left-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
border-bottom-right-radius: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
padding: 16px 24px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
width: 300px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<SettingsRawTable :columns="columns" :data="data">
|
||||||
|
<template #cell(service_notifications)="{ row }">
|
||||||
|
<div class="d-flex align-items-center text-clr-grey-600">
|
||||||
|
<Component :is="resolveComponent(`ui-icon-${row.original.icon}`)" />
|
||||||
|
<h4 class="ml-10">
|
||||||
|
{{ row.original.service_notifications }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(telegram)="{ row }">
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<UiCheckbox id="telegram" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(mail)="{ row }">
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<UiCheckbox id="mail" :model-value="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(push)="{ row }">
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<UiCheckbox id="push" :model-value="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SettingsRawTable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { createColumnHelper } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper()
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'service_notifications',
|
||||||
|
header: 'Service Notifications',
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'telegram',
|
||||||
|
header: 'Telegram',
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'mail',
|
||||||
|
header: 'Mail',
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'push',
|
||||||
|
header: 'Push',
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{ service_notifications: 'Вход в аккаунт', icon: 'signin' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
48
apps/client/components/settings/tariff-card.vue
Normal file
48
apps/client/components/settings/tariff-card.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="settings-tariff-card">
|
||||||
|
<div class="settings-tariff-card__header">
|
||||||
|
<div>
|
||||||
|
<slot name="icon" />
|
||||||
|
<h4 class="d-inline-block ml-8 text-clr-grey-600">
|
||||||
|
{{ title }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot name="subtitle" />
|
||||||
|
</div>
|
||||||
|
<div class="settings-tariff-card__content">
|
||||||
|
<slot name="content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.settings-tariff-card {
|
||||||
|
&__header{
|
||||||
|
padding: 24px;
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
background-color: $clr-grey-100;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content{
|
||||||
|
padding: 24px;
|
||||||
|
border-bottom-left-radius: 12px;
|
||||||
|
border-bottom-right-radius: 12px;
|
||||||
|
border: 2px solid $clr-grey-100;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
87
apps/client/components/sidebar.vue
Normal file
87
apps/client/components/sidebar.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<VueFinalModal
|
||||||
|
:modal-id="modalId"
|
||||||
|
class="sidebar"
|
||||||
|
content-class="sidebar__content"
|
||||||
|
content-transition="sidebar"
|
||||||
|
hide-overlay
|
||||||
|
background="interactive"
|
||||||
|
:click-to-close="false"
|
||||||
|
:z-index-fn="({ index }) => 6000 + 2 * index"
|
||||||
|
>
|
||||||
|
<div class="sidebar__top">
|
||||||
|
<UiButton icon="arrow-left" type="outlined" color="secondary" @click="vfm.close(modalId)" />
|
||||||
|
|
||||||
|
<slot name="top" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.default" class="sidebar__middle">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.bottom" class="sidebar__bottom">
|
||||||
|
<slot name="bottom" />
|
||||||
|
</div>
|
||||||
|
</VueFinalModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { VueFinalModal, useVfm } from 'vue-final-modal'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const vfm = useVfm()
|
||||||
|
|
||||||
|
const modalId = computed(() => {
|
||||||
|
if (props.id)
|
||||||
|
return `sidebar-${props.id}`
|
||||||
|
|
||||||
|
return 'sidebar'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.sidebar {
|
||||||
|
&__content {
|
||||||
|
width: 353px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: $clr-white;
|
||||||
|
height: 100%;
|
||||||
|
gap: 32px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__middle {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-active,
|
||||||
|
&-leave-active {
|
||||||
|
transition: transform .2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-to,
|
||||||
|
&-leave-from {
|
||||||
|
//transform: rotateZ(360deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-from,
|
||||||
|
&-leave-to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
apps/client/components/static-error.vue
Normal file
9
apps/client/components/static-error.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<Transition name="fade">
|
||||||
|
<UiAlert v-if="staticError" type="negative" :text="staticError.message" />
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const staticError = useStaticError()
|
||||||
|
</script>
|
||||||
139
apps/client/components/stepper.vue
Normal file
139
apps/client/components/stepper.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-stepper">
|
||||||
|
<template
|
||||||
|
v-for="(label, index) in items"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=" form-stepper__step"
|
||||||
|
|
||||||
|
:class="{
|
||||||
|
'form-stepper__step--current': index === step,
|
||||||
|
'form-stepper__step--passed': index < step,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="form-stepper__index">
|
||||||
|
<span v-if="index > step - 1 ">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<UiIconCheck
|
||||||
|
v-else
|
||||||
|
class="form-stepper__icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="form-stepper__label"
|
||||||
|
v-html="label"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="index !== items.length - 1"
|
||||||
|
class="form-stepper__line"
|
||||||
|
:class="{
|
||||||
|
'form-stepper__line--passed': index > step - 1,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['set'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.form-stepper {
|
||||||
|
$self: &;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__line {
|
||||||
|
$line: &;
|
||||||
|
|
||||||
|
height: 3px;
|
||||||
|
flex: 1;
|
||||||
|
background-image: linear-gradient(90deg, #a5e9bc 0%, #a7b9d5 100%);
|
||||||
|
margin-inline: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
&--passed {
|
||||||
|
background-image: linear-gradient(90deg, #a7b9d5 0%, #dfe5ff 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__step {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
justify-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
justify-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--passed {
|
||||||
|
--form-stepper-index-background: #{$clr-green-500};
|
||||||
|
--form-stepper-label-color: #{$clr-green-500};
|
||||||
|
|
||||||
|
//color: var(--form-stepper-index-color, #{$clr-white});
|
||||||
|
|
||||||
|
cursor: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--current {
|
||||||
|
--form-stepper-index-background: #{$clr-grey-300};
|
||||||
|
--form-stepper-label-color: #{$clr-grey-600};
|
||||||
|
--form-stepper-index-color: #{$clr-grey-600};
|
||||||
|
cursor: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
--icon-size: 20px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: $clr-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__index {
|
||||||
|
@include txt-i-m;
|
||||||
|
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--form-stepper-index-background, #{$clr-grey-200});
|
||||||
|
color: var(--form-stepper-index-color, #{$clr-grey-400});
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
@include txt-r;
|
||||||
|
|
||||||
|
color: var(--form-stepper-label-color, #{$clr-grey-600});
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
140
apps/client/components/stripped-table.vue
Normal file
140
apps/client/components/stripped-table.vue
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<UiPlainTable
|
||||||
|
class="stripped-table"
|
||||||
|
:class="{ 'is-loading': loading }"
|
||||||
|
:columns="columns"
|
||||||
|
:data="data"
|
||||||
|
>
|
||||||
|
<template v-if="loading" #default>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="i in 6" :key="i">
|
||||||
|
<td :colspan="columns.length">
|
||||||
|
<div class="stripped-table__skeleton">
|
||||||
|
<div
|
||||||
|
v-for="j in i % 2 === 0 ? 7 : 3"
|
||||||
|
:key="j"
|
||||||
|
class="stripped-table__skeleton-cell"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!data.length" #default>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="stripped-table__no-data" :colspan="columns.length">
|
||||||
|
<img src="/no-data.svg" alt="No Data">
|
||||||
|
<h4>No Data</h4>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-for="(_, slot) of $slots" #[slot]="scope">
|
||||||
|
<slot :name="slot" v-bind="scope" />
|
||||||
|
</template>
|
||||||
|
</UiPlainTable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ColumnDef } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
columns: ColumnDef<unknown>[]
|
||||||
|
data: unknown[]
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
loading: false,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.stripped-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0 4px;
|
||||||
|
text-align: left;
|
||||||
|
margin-block: -4px;
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px 8px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
border-bottom-left-radius: 12px;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
border-bottom-right-radius: 12px;
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
@include h5;
|
||||||
|
|
||||||
|
background-color: $clr-grey-100;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-loading {
|
||||||
|
td {
|
||||||
|
background-color: $clr-grey-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.is-loading) {
|
||||||
|
tbody {
|
||||||
|
tr:nth-child(even) {
|
||||||
|
td {
|
||||||
|
background-color: $clr-grey-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__no-data {
|
||||||
|
height: 494px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: $clr-grey-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__skeleton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__skeleton-cell {
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: $clr-white;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
110deg,
|
||||||
|
$clr-white 8%,
|
||||||
|
$clr-grey-100 18%,
|
||||||
|
$clr-white 33%
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
height: 36px;
|
||||||
|
animation: 1.5s shine linear infinite;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine {
|
||||||
|
to {
|
||||||
|
background-position-x: -200%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
163
apps/client/components/verification/base-card.vue
Normal file
163
apps/client/components/verification/base-card.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="verification-card"
|
||||||
|
:class="{
|
||||||
|
'verification-card--passed': passed,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="verification-card__header">
|
||||||
|
<UiBadge
|
||||||
|
v-if="badge"
|
||||||
|
:type="badge.type"
|
||||||
|
class="mb-24"
|
||||||
|
>
|
||||||
|
{{ badge.title }}
|
||||||
|
</UiBadge>
|
||||||
|
|
||||||
|
<p class="verification-card-info__title">
|
||||||
|
Withdrawal
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="info"
|
||||||
|
class="verification-card-info"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span class="verification-card-info__per-month-amount">
|
||||||
|
{{ info.perMonth }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="verification-card-info__per-month-text"> / month </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<span class="verification-card-info__per-day-amount">
|
||||||
|
{{ info.perDay }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="verification-card-info__per-day-text"> / day </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="verification-card-info"
|
||||||
|
>
|
||||||
|
<span class="verification-card-info__per-month-amount">
|
||||||
|
No limit
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="verification-card-info__per-month-text"> / month & day </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="verification-card-body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="verification-card__action">
|
||||||
|
<UiAlert
|
||||||
|
v-if="passed"
|
||||||
|
type="positive"
|
||||||
|
text="Your current status"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
v-else
|
||||||
|
class="w-100"
|
||||||
|
size="large"
|
||||||
|
:href="link"
|
||||||
|
>
|
||||||
|
Start verification
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
passed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
type: String,
|
||||||
|
default: '#',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.verification-card {
|
||||||
|
$self: &;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: var(--verification-card-width, 405px);
|
||||||
|
//height: 585px;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
height: 157px;
|
||||||
|
border-bottom: 1px solid $clr-grey-400;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--passed {
|
||||||
|
background-color: $clr-white;
|
||||||
|
outline: $clr-green-500 solid 2px;
|
||||||
|
outline-offset: -2px;
|
||||||
|
|
||||||
|
.verification-card__title {
|
||||||
|
color: $clr-black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-card-info {
|
||||||
|
&__per-month-amount {
|
||||||
|
@include h2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__per-month-text {
|
||||||
|
@include txt-l-sb;
|
||||||
|
color: $clr-grey-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__per-day-amount {
|
||||||
|
@include txt-l-sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__per-day-text {
|
||||||
|
@include txt-l-sb;
|
||||||
|
color: $clr-grey-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
@include txt-l-m;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
55
apps/client/components/verification/basic.vue
Normal file
55
apps/client/components/verification/basic.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-for="block in propertyBlocks"
|
||||||
|
:key="block.title"
|
||||||
|
class="elements"
|
||||||
|
>
|
||||||
|
<span class="title">
|
||||||
|
{{ block.title }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h5 class="text-clr-grey-500">
|
||||||
|
{{ block.underTitle }}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<VerificationProperty
|
||||||
|
v-for="property in block.properties"
|
||||||
|
:key="property.title"
|
||||||
|
:title="property.title"
|
||||||
|
:text="property.text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const propertyBlocks=[
|
||||||
|
{
|
||||||
|
title: 'Restrictions',
|
||||||
|
underTitle: 'Invoices',
|
||||||
|
properties:[
|
||||||
|
{title:'Minimum amount in the invoice is', text:' 5 USDT'},
|
||||||
|
{title:'Commission for creating an invoice is', text:'0.5 USDT'},
|
||||||
|
{title:'Limit for creating invoices is', text:'200 per day, 1000 per month'},
|
||||||
|
]},
|
||||||
|
{
|
||||||
|
underTitle: 'Withdrawal of funds',
|
||||||
|
properties:[
|
||||||
|
{title:'Minimum withdrawal amount is', text:'5 USDT'},
|
||||||
|
{title:'Commission for withdrawal is', text:'2.5 USDT + 1%'}
|
||||||
|
]},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.elements {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title{
|
||||||
|
@include txt-m-b;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
78
apps/client/components/verification/extended.vue
Normal file
78
apps/client/components/verification/extended.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="elements mb-16">
|
||||||
|
<span class="requirements"> Requirements </span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<UiIconSSocialCard class="mr-4 text-clr-grey-400 d-inline-block" />
|
||||||
|
<span class="text"> Organization data </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<UiIconSExcursion class="mr-4 text-clr-grey-400" />
|
||||||
|
<span class="text">Identity verification </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="block in propertyBlocks"
|
||||||
|
:key="block.title"
|
||||||
|
class="elements"
|
||||||
|
>
|
||||||
|
<span class="title">
|
||||||
|
{{ block.title }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h5 class="text-clr-grey-500">
|
||||||
|
{{ block.underTitle }}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<VerificationProperty
|
||||||
|
v-for="property in block.properties"
|
||||||
|
:key="property.title"
|
||||||
|
:title="property.title"
|
||||||
|
:text="property.text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const propertyBlocks = [
|
||||||
|
{
|
||||||
|
title: 'Restrictions',
|
||||||
|
underTitle: 'Invoices',
|
||||||
|
properties: [
|
||||||
|
{ title: 'Invoice creation', text: 'unlimited' },
|
||||||
|
{ title: 'Commission for creating an invoice is', text: '0.5 USDT' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
underTitle: 'Withdrawal of funds',
|
||||||
|
properties: [
|
||||||
|
{ title: 'Commission for withdrawal is', text: '2.5 USDT + 1%' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.elements {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title{
|
||||||
|
@include txt-m-b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text{
|
||||||
|
@include txt-r-m;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.requirements{
|
||||||
|
@include txt-m-b;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
apps/client/components/verification/property.vue
Normal file
49
apps/client/components/verification/property.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div class="varification-property">
|
||||||
|
<Component
|
||||||
|
:is="resolveComponent(`ui-icon-${icon}`)"
|
||||||
|
class="mr-4"
|
||||||
|
:class="icon === 'SCheck' ? 'text-clr-green-500' : ''"
|
||||||
|
/>
|
||||||
|
<span class="varification-property__title">
|
||||||
|
|
||||||
|
{{ title }}
|
||||||
|
|
||||||
|
<span class="varification-property__text">
|
||||||
|
{{ text }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: 'SCheck',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.varification-property {
|
||||||
|
|
||||||
|
color: $clr-grey-500;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
@include txt-r;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
@include txt-r-b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
40
apps/client/components/verification/we-accept.vue
Normal file
40
apps/client/components/verification/we-accept.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p class="mb-16">
|
||||||
|
<strong> We accept: </strong>
|
||||||
|
</p>
|
||||||
|
<ul class="accept-list">
|
||||||
|
<li
|
||||||
|
v-for="listItem in list"
|
||||||
|
:key="listItem.title"
|
||||||
|
>
|
||||||
|
<span> {{ listItem.title }} </span>
|
||||||
|
<p class="text-clr-grey-500">
|
||||||
|
{{ listItem.underTitle }}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
list: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.accept-list{
|
||||||
|
list-style-type: disc;
|
||||||
|
padding: 0 0 0 24px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
39
apps/client/composables/api.ts
Normal file
39
apps/client/composables/api.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { NitroFetchRequest } from 'nitropack'
|
||||||
|
import { callWithNuxt } from '#app'
|
||||||
|
|
||||||
|
export function $api<
|
||||||
|
T = unknown,
|
||||||
|
R extends NitroFetchRequest = NitroFetchRequest,
|
||||||
|
>(
|
||||||
|
request: Parameters<typeof $fetch<T, R>>[0],
|
||||||
|
options?: Partial<Parameters<typeof $fetch<T, R>>[1]>,
|
||||||
|
) {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
const cookies = useRequestHeaders(['cookie'])
|
||||||
|
|
||||||
|
return $fetch<T, R>(request, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options?.headers,
|
||||||
|
...cookies,
|
||||||
|
},
|
||||||
|
retry: false,
|
||||||
|
baseURL: runtimeConfig.public.apiHost as string,
|
||||||
|
credentials: 'include',
|
||||||
|
onResponseError: async ({ response }) => {
|
||||||
|
if (response.status === 401) {
|
||||||
|
nuxtApp.runWithContext(() => {
|
||||||
|
useCookie('session').value = null
|
||||||
|
})
|
||||||
|
await callWithNuxt(nuxtApp, clearNuxtState, ['user'])
|
||||||
|
await callWithNuxt(nuxtApp, navigateTo, ['/login', { redirectCode: 401 }])
|
||||||
|
}
|
||||||
|
|
||||||
|
// setStaticError({
|
||||||
|
// status: response.status,
|
||||||
|
// message: nuxtApp.$i18n.t('something_went_wrong'),
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
16
apps/client/composables/static-error.ts
Normal file
16
apps/client/composables/static-error.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export interface StaticError {
|
||||||
|
status: number
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStaticError() {
|
||||||
|
return useState<StaticError | undefined>('static-error')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setStaticError(value?: StaticError) {
|
||||||
|
useStaticError().value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearStaticError() {
|
||||||
|
clearNuxtState(['static-error'])
|
||||||
|
}
|
||||||
69
apps/client/composables/use-auth.ts
Normal file
69
apps/client/composables/use-auth.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
export interface User {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
const user = useState<User | null>('user')
|
||||||
|
|
||||||
|
const authenticated = computed(() => !!user.value)
|
||||||
|
|
||||||
|
async function getUser() {
|
||||||
|
user.value = await $api('/users/current', { method: 'GET' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(email: string, password: string) {
|
||||||
|
await $api('/sessions', { method: 'POST', body: { email, password } })
|
||||||
|
await getUser()
|
||||||
|
|
||||||
|
navigateTo('/projects')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function register(email: string, password: string) {
|
||||||
|
await $api('/users', { method: 'POST', body: { email, password } })
|
||||||
|
|
||||||
|
navigateTo('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
await $api('/sessions', { method: 'delete', body: {} })
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
clearNuxtState('user')
|
||||||
|
navigateTo('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestResetPassword(email: string) {
|
||||||
|
await $api('/users/password_reset', { method: 'post', body: { email } })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetPassword(newPassword: string, resetCode: string) {
|
||||||
|
await $api('/users/password_reset', {
|
||||||
|
method: 'put',
|
||||||
|
body: {
|
||||||
|
newPassword,
|
||||||
|
resetCode,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async function resendVerificationCode(email: string) {
|
||||||
|
await $api('/users/verification', {
|
||||||
|
method: 'put',
|
||||||
|
body: { email },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
authenticated,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
resendVerificationCode,
|
||||||
|
requestResetPassword,
|
||||||
|
resetPassword,
|
||||||
|
}
|
||||||
|
}
|
||||||
156
apps/client/composables/use-filters.ts
Normal file
156
apps/client/composables/use-filters.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { computed, provide, reactive, unref } from 'vue'
|
||||||
|
import type { ComputedRef, InjectionKey, MaybeRef } from 'vue'
|
||||||
|
import { omit } from 'lodash-es'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
const DATE_FORMAT = 'DD-MM-YYYY'
|
||||||
|
|
||||||
|
export interface Filter {
|
||||||
|
type?: 'select' | 'calendar'
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
placeholder: string
|
||||||
|
searchable?: boolean
|
||||||
|
multiple?: boolean
|
||||||
|
options?: { label?: string, value: unknown }[]
|
||||||
|
transform?: (value: AppliedFilter) => AppliedFilters
|
||||||
|
}
|
||||||
|
export type Filters = Filter[]
|
||||||
|
|
||||||
|
export type AppliedFilter = null | string | string[]
|
||||||
|
export type AppliedFilters = Record<string, AppliedFilter>
|
||||||
|
|
||||||
|
export interface FiltersContext {
|
||||||
|
schema: MaybeRef<Filters>
|
||||||
|
appliedFiltersRaw: AppliedFilters
|
||||||
|
appliedFilters: ComputedRef<AppliedFilters>
|
||||||
|
empty: boolean | ComputedRef<boolean>
|
||||||
|
apply: (p1: AppliedFilters | string, p2?: AppliedFilter) => void
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filtersContextKey: InjectionKey<FiltersContext>
|
||||||
|
= Symbol('FILTERS')
|
||||||
|
|
||||||
|
export default (filters: MaybeRef<Filters>) => {
|
||||||
|
const url = useRequestURL()
|
||||||
|
|
||||||
|
const searchString = computed(() => url.search)
|
||||||
|
|
||||||
|
const parsedUrl: { other: Record<string, string>, filters: AppliedFilters }
|
||||||
|
= reactive({
|
||||||
|
other: {},
|
||||||
|
filters: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const allowedFilters = computed<string[]>(() => {
|
||||||
|
return unref(filters).map(filter => filter.key)
|
||||||
|
})
|
||||||
|
|
||||||
|
parseUrl(searchString.value)
|
||||||
|
|
||||||
|
const appliedFiltersRaw = reactive<AppliedFilters>({
|
||||||
|
...Object.fromEntries(
|
||||||
|
allowedFilters.value.map(key => [key, isMultiple(key) ? [] : null]),
|
||||||
|
),
|
||||||
|
...parsedUrl.filters,
|
||||||
|
})
|
||||||
|
|
||||||
|
const appliedFilters = computed<AppliedFilters>(() => {
|
||||||
|
return Object.entries(appliedFiltersRaw).reduce((result, [key, value]) => {
|
||||||
|
const filter = getFilterByKey(key)!
|
||||||
|
if (filter.transform) {
|
||||||
|
const transformedValue = filter.transform(value)
|
||||||
|
|
||||||
|
if (transformedValue)
|
||||||
|
result = { ...result, ...transformedValue }
|
||||||
|
else
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, {} as AppliedFilters)
|
||||||
|
})
|
||||||
|
|
||||||
|
const empty = computed(() =>
|
||||||
|
Object.values(appliedFiltersRaw).every((value) => {
|
||||||
|
return Array.isArray(value) ? value.length === 0 : value !== null
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
function parseUrl(searchString: string) {
|
||||||
|
const params = new URLSearchParams(searchString)
|
||||||
|
|
||||||
|
parsedUrl.other = {}
|
||||||
|
parsedUrl.filters = {}
|
||||||
|
|
||||||
|
for (const [key, value] of Array.from(params.entries())) {
|
||||||
|
if (allowedFilters.value.includes(key)) {
|
||||||
|
let newValue = isMultiple(key) ? value.split(',') : value
|
||||||
|
|
||||||
|
if (isCalendar(key)) {
|
||||||
|
newValue = [...newValue].map(date =>
|
||||||
|
dayjs(date, DATE_FORMAT).valueOf().toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedUrl.filters[key] = newValue
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
parsedUrl.other[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedUrl.other = omit(parsedUrl.other, ['page'])
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply(p1: AppliedFilters | string, p2?: AppliedFilter) {
|
||||||
|
if (p2 && typeof p1 === 'string') {
|
||||||
|
appliedFiltersRaw[p1] = p2
|
||||||
|
}
|
||||||
|
else if (typeof p1 === 'object') {
|
||||||
|
for (const [key, value] of Object.entries(p1))
|
||||||
|
appliedFiltersRaw[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
for (const key of Object.keys(appliedFiltersRaw))
|
||||||
|
appliedFiltersRaw[key] = isMultiple(key) ? [] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterByKey(key: string) {
|
||||||
|
return unref(filters).find(f => f.key === key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMultiple(key: string) {
|
||||||
|
const filter = getFilterByKey(key)
|
||||||
|
|
||||||
|
return filter?.multiple ?? filter?.type === 'calendar' ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCalendar(key: string) {
|
||||||
|
const filter = getFilterByKey(key)
|
||||||
|
|
||||||
|
return filter?.type === 'calendar' ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(filtersContextKey, {
|
||||||
|
schema: filters,
|
||||||
|
appliedFiltersRaw,
|
||||||
|
appliedFilters,
|
||||||
|
empty,
|
||||||
|
apply,
|
||||||
|
reset,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
appliedFiltersRaw,
|
||||||
|
appliedFilters,
|
||||||
|
empty,
|
||||||
|
apply,
|
||||||
|
reset,
|
||||||
|
}
|
||||||
|
}
|
||||||
11
apps/client/eslint.config.js
Normal file
11
apps/client/eslint.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import antfu from '@antfu/eslint-config'
|
||||||
|
|
||||||
|
export default await antfu({
|
||||||
|
overrides: {
|
||||||
|
vue: {
|
||||||
|
'vue/block-order': ['error', {
|
||||||
|
order: ['template', 'script', 'style'],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
23
apps/client/helpers/invoices.ts
Normal file
23
apps/client/helpers/invoices.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { AlertType } from 'ui-layer/components/alert/types'
|
||||||
|
import type { UiIcon } from '#build/types/ui/icons'
|
||||||
|
|
||||||
|
export function getStatusType(status: string): AlertType {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
return 'positive'
|
||||||
|
case 'expired':
|
||||||
|
return 'negative'
|
||||||
|
default:
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function getStatusIcon(status: string): UiIcon {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed':
|
||||||
|
return 's-check'
|
||||||
|
case 'expired':
|
||||||
|
return 's-cross'
|
||||||
|
default:
|
||||||
|
return 's-clock'
|
||||||
|
}
|
||||||
|
}
|
||||||
5
apps/client/i18n.config.ts
Normal file
5
apps/client/i18n.config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default defineI18nConfig(() => ({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'en',
|
||||||
|
globalInjection: true,
|
||||||
|
}))
|
||||||
7
apps/client/index.d.ts
vendored
Normal file
7
apps/client/index.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
declare module '#app' {
|
||||||
|
interface PageMeta {
|
||||||
|
centerContent?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
122
apps/client/lang/en.js
Normal file
122
apps/client/lang/en.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
export default {
|
||||||
|
validation: {
|
||||||
|
required: 'This field is required',
|
||||||
|
email: 'Enter an email address in the format example{\'@\'}xxxx.xxx',
|
||||||
|
confirmed: 'Passwords must match',
|
||||||
|
password:
|
||||||
|
'Password must contain at least 8 characters, including uppercase letters, numbers, and special characters',
|
||||||
|
max: 'The value must be less than or equal to {0} characters.',
|
||||||
|
url: 'Please enter a valid URL',
|
||||||
|
},
|
||||||
|
field_max_characters: '{0} (maximum {1} characters)',
|
||||||
|
|
||||||
|
invoice_status: {
|
||||||
|
completed: 'Success',
|
||||||
|
expired: 'Expired',
|
||||||
|
pending: 'Pending',
|
||||||
|
awaiting_payment: 'Awaiting',
|
||||||
|
investigating: 'Investigating',
|
||||||
|
broadcasted: 'Broadcasted',
|
||||||
|
overpaid: 'Overpaid',
|
||||||
|
underpaid: 'Underpaid',
|
||||||
|
},
|
||||||
|
|
||||||
|
something_went_wrong: 'Something went wrong',
|
||||||
|
invalid_otp_code: 'Invalid OTP Code',
|
||||||
|
create: 'Create',
|
||||||
|
change: 'Change',
|
||||||
|
select: 'Select',
|
||||||
|
register: 'Register',
|
||||||
|
login: 'Login',
|
||||||
|
privacy_policy: 'Privacy Policy',
|
||||||
|
copyright: '© {year}, Indefiti - the best crypto processing',
|
||||||
|
support: 'Support',
|
||||||
|
network: 'Network',
|
||||||
|
maximum: 'Maximum',
|
||||||
|
continue: 'Continue',
|
||||||
|
fee: 'Fee',
|
||||||
|
login_greeting: 'Hello!',
|
||||||
|
register_greeting: 'Register with Indefiti',
|
||||||
|
to_main: 'To home page',
|
||||||
|
back: 'Back',
|
||||||
|
back_to: 'Back to {0}',
|
||||||
|
next: 'Next',
|
||||||
|
withdraw: 'Withdraw',
|
||||||
|
email: 'E-mail',
|
||||||
|
password: 'Password',
|
||||||
|
repeat_password: 'Repeat Password',
|
||||||
|
what_happened: 'What happened?',
|
||||||
|
what_todo: 'I don\'t remember what to do',
|
||||||
|
write_us: 'Write to us',
|
||||||
|
reset_password: 'Reset Password',
|
||||||
|
lost_password: 'Forgot Password?',
|
||||||
|
can_reset_password: 'Password can be reset',
|
||||||
|
fill_the_form: 'fill out this form',
|
||||||
|
reset_password_alert:
|
||||||
|
'For security reasons, withdrawals will be prohibited for 24 hours after changing the password',
|
||||||
|
check_email: 'Check your e-mail',
|
||||||
|
email_confirmation: 'We have sent you an e-mail to {0}',
|
||||||
|
register_email_instructions:
|
||||||
|
'To confirm your email,<br>follow the link in the email',
|
||||||
|
login_email_instructions:
|
||||||
|
'To reset your password, follow the link in the email',
|
||||||
|
send_again: 'Send again',
|
||||||
|
spam: 'Spam',
|
||||||
|
|
||||||
|
create_your_project: {
|
||||||
|
title: '{0} your first project',
|
||||||
|
create: 'Create',
|
||||||
|
content: 'This is necessary to accept payments on the site',
|
||||||
|
},
|
||||||
|
|
||||||
|
your_invoices_will_be_displayed_here: {
|
||||||
|
title: 'Your invoices will be displayed here',
|
||||||
|
content: 'Click on the create invoice button to start invoicing',
|
||||||
|
},
|
||||||
|
create_an_invoice: 'Сreate an invoice',
|
||||||
|
|
||||||
|
forgot_password_questions: {
|
||||||
|
did_not_get_mail: {
|
||||||
|
title: 'Check the {0} folder if you don\'t see the email',
|
||||||
|
content: 'Didn\'t receive the email? {0}',
|
||||||
|
},
|
||||||
|
|
||||||
|
forgot_password: {
|
||||||
|
title: 'Forgot my password',
|
||||||
|
content:
|
||||||
|
'{0}, if you have access to your email. If you don\'t have access, {1}.',
|
||||||
|
},
|
||||||
|
|
||||||
|
access_to_mail: {
|
||||||
|
title: 'No access to email',
|
||||||
|
content: 'If you don\'t have access to your email, {0}.',
|
||||||
|
},
|
||||||
|
|
||||||
|
no_mail: {
|
||||||
|
title: 'The website says there is no such email',
|
||||||
|
content:
|
||||||
|
'Check which email you received receipts and other notifications to. If you are sure you are entering everything correctly, {0}.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
sign_up_agreement: {
|
||||||
|
base: 'I confirm my agreement with the {0} and {1}',
|
||||||
|
privacy_policy: 'Privacy Policy',
|
||||||
|
user_agreement: 'User Agreement',
|
||||||
|
},
|
||||||
|
apply: 'Apply',
|
||||||
|
try: 'Try',
|
||||||
|
test_functional: 'Test functional',
|
||||||
|
learn_functional:
|
||||||
|
'Here you can familiarize yourself with the main features. To get started with payments, create your first project.',
|
||||||
|
how_does_it_works: 'How does it works',
|
||||||
|
projects: 'Projects',
|
||||||
|
account_created_successfully: 'Account created successfully',
|
||||||
|
account_verification_error: 'An error occurred during account verification',
|
||||||
|
try_again_or_contact_support: 'Try again or contact support',
|
||||||
|
we_have_sent_you_an_email_to: 'We have sent you an email to {0}',
|
||||||
|
check_spam_folder: 'Check the {0} folder if you do not see the letter',
|
||||||
|
did_not_get_mail: 'Did not get the email?',
|
||||||
|
reset_password_instructions:
|
||||||
|
'To reset your password, follow the link from the letter',
|
||||||
|
}
|
||||||
1
apps/client/lang/ru.js
Normal file
1
apps/client/lang/ru.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default {}
|
||||||
131
apps/client/layouts/auth.vue
Normal file
131
apps/client/layouts/auth.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div class="auth-layout">
|
||||||
|
<header class="auth-header">
|
||||||
|
<NuxtLink to="/projects">
|
||||||
|
<img
|
||||||
|
src="/logo.svg"
|
||||||
|
alt="logo"
|
||||||
|
draggable="false"
|
||||||
|
>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
v-if="headerAction"
|
||||||
|
class="ml-a"
|
||||||
|
type="ghost"
|
||||||
|
:href="headerAction.link"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{{ headerAction.name }}
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<LangSwitcher />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="auth-main">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="auth-footer">
|
||||||
|
<div class="auth-footer__left">
|
||||||
|
<UiButton
|
||||||
|
type="link"
|
||||||
|
color="secondary"
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
{{ $t('privacy_policy') }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-footer__middle">
|
||||||
|
<span class="auth-footer__copyright">
|
||||||
|
{{ $t('copyright', { year }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-footer__right">
|
||||||
|
<UiButton
|
||||||
|
icon="circle-question"
|
||||||
|
type="link"
|
||||||
|
color="secondary"
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
{{ $t('support') }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute()
|
||||||
|
const { t } = useI18n({ useScope: 'global' })
|
||||||
|
const year = computed(() => new Date().getFullYear())
|
||||||
|
|
||||||
|
const headerAction = computed(() => {
|
||||||
|
switch (route.name) {
|
||||||
|
case 'login':
|
||||||
|
return {
|
||||||
|
name: t('register'),
|
||||||
|
link: '/register',
|
||||||
|
}
|
||||||
|
case 'register':
|
||||||
|
return {
|
||||||
|
name: t('login'),
|
||||||
|
link: '/login',
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.auth-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header {
|
||||||
|
display: flex;
|
||||||
|
padding: 32px 56px 0 56px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-block: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer {
|
||||||
|
padding: 24px 56px 32px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__left {
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__middle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__right {
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__copyright {
|
||||||
|
color: $clr-grey-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
117
apps/client/layouts/default.vue
Normal file
117
apps/client/layouts/default.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div class="default-layout">
|
||||||
|
<NavigationSidebar>
|
||||||
|
<template #logo>
|
||||||
|
<NavigationLogo />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default>
|
||||||
|
<NavigationItem
|
||||||
|
v-for="item in navItems"
|
||||||
|
:key="item.to"
|
||||||
|
:to="item.to"
|
||||||
|
:icon="item.icon"
|
||||||
|
:title="item.title"
|
||||||
|
:matcher="item.matcher ?? defaultMatcher(item.to)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #bottom>
|
||||||
|
<NavigationItem
|
||||||
|
icon="info"
|
||||||
|
title="Help"
|
||||||
|
to="/_"
|
||||||
|
:matcher="() => ['/help'].includes(route.fullPath)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</NavigationSidebar>
|
||||||
|
|
||||||
|
<main
|
||||||
|
class="default-layout__main"
|
||||||
|
:class="{
|
||||||
|
center: shouldCenterContent,
|
||||||
|
narrow: isAnySidebarOpened,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="container">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<ModalsContainer />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ModalsContainer, useVfm } from 'vue-final-modal'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const route = useRoute()
|
||||||
|
const { openedModals } = useVfm()
|
||||||
|
|
||||||
|
const defaultMatcher = link => () => route.fullPath === link
|
||||||
|
|
||||||
|
const navItems = computed(() => [
|
||||||
|
{
|
||||||
|
icon: 'merchant',
|
||||||
|
title: t('projects'),
|
||||||
|
to: '/projects',
|
||||||
|
matcher: () =>
|
||||||
|
route.fullPath === '/projects'
|
||||||
|
|| route.fullPath.startsWith('/projects')
|
||||||
|
|| route.meta.alias?.startsWith('/projects'),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const shouldCenterContent = computed(() => route.meta.centerContent)
|
||||||
|
const isAnySidebarOpened = computed(() => openedModals.some(modal => modal.value.modalId.startsWith('sidebar') && modal.value.overlayVisible.value))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.default-layout {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 120px 1fr;
|
||||||
|
|
||||||
|
grid-area: sidebar;
|
||||||
|
overflow-y: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 0 35px 32px;
|
||||||
|
transition: padding-right .2s ease-in-out;
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
padding-top: 32px;
|
||||||
|
|
||||||
|
> .container {
|
||||||
|
margin-block: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.narrow {
|
||||||
|
padding-right: calc(353px + 35px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.center) {
|
||||||
|
> .container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1250px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
5
apps/client/layouts/empty.vue
Normal file
5
apps/client/layouts/empty.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="empty-layout">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
7
apps/client/middleware/01.slash.global.ts
Normal file
7
apps/client/middleware/01.slash.global.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
if (to.path === '/')
|
||||||
|
return navigateTo('/projects')
|
||||||
|
|
||||||
|
if (to.path !== '/' && to.path.endsWith('/'))
|
||||||
|
return navigateTo(to.path.slice(0, -1))
|
||||||
|
})
|
||||||
17
apps/client/middleware/02.load-user.global.ts
Normal file
17
apps/client/middleware/02.load-user.global.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { User } from '~/composables/use-auth'
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
const session = useCookie('session')
|
||||||
|
const { authenticated, user } = useAuth()
|
||||||
|
|
||||||
|
if (session.value && !authenticated.value) {
|
||||||
|
try {
|
||||||
|
user.value = await $api<User>('/users/current', {
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
6
apps/client/middleware/auth.ts
Normal file
6
apps/client/middleware/auth.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
const { authenticated } = useAuth()
|
||||||
|
|
||||||
|
if (!authenticated.value)
|
||||||
|
return navigateTo('/login')
|
||||||
|
})
|
||||||
6
apps/client/middleware/guest-only.ts
Normal file
6
apps/client/middleware/guest-only.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
|
const { authenticated } = useAuth()
|
||||||
|
|
||||||
|
if (authenticated.value)
|
||||||
|
return navigateTo('/projects')
|
||||||
|
})
|
||||||
48
apps/client/nuxt.config.ts
Normal file
48
apps/client/nuxt.config.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
extends: ['../../layers/shared', '../../layers/ui'],
|
||||||
|
modules: [
|
||||||
|
'@pinia/nuxt',
|
||||||
|
[
|
||||||
|
'@nuxtjs/i18n',
|
||||||
|
{
|
||||||
|
vueI18n: './i18n.config.ts',
|
||||||
|
lazy: true,
|
||||||
|
langDir: 'lang',
|
||||||
|
compilation: {
|
||||||
|
strictMessage: false,
|
||||||
|
},
|
||||||
|
locales: [
|
||||||
|
// {
|
||||||
|
// code: 'ru',
|
||||||
|
// name: 'Русский',
|
||||||
|
// file: 'ru.js',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
code: 'en',
|
||||||
|
name: 'English',
|
||||||
|
file: 'en.js',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultLocale: 'en',
|
||||||
|
strategy: 'no_prefix',
|
||||||
|
detectBrowserLanguage: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
css: ['~/assets/styles.scss', 'vue-final-modal/style.css'],
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
host: process.env.NODE_ENV === 'development'
|
||||||
|
? 'http://localhost:3000'
|
||||||
|
: 'https://app.prgms.io',
|
||||||
|
payHost: process.env.NODE_ENV === 'development'
|
||||||
|
? 'http://localhost:3001'
|
||||||
|
: 'https://pay.prgms.io',
|
||||||
|
apiHost:
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
|
? '/api'
|
||||||
|
: 'https://api.prgms.io/api/v1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
35
apps/client/package.json
Normal file
35
apps/client/package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@pinia/nuxt": "^0.4.11",
|
||||||
|
"@vueuse/core": "^10.7.0",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
|
"decimal.js": "^10.4.3",
|
||||||
|
"defu": "^6.1.2",
|
||||||
|
"ufo": "^1.3.2",
|
||||||
|
"ui-layer": "*",
|
||||||
|
"shared-layer": "*",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"vue-final-modal": "^4.4.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^2.1.2",
|
||||||
|
"@nuxt/devtools": "latest",
|
||||||
|
"@nuxtjs/i18n": "^8.0.0-rc.5",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"nuxt": "latest",
|
||||||
|
"sass": "^1.69.0",
|
||||||
|
"unplugin-vue-components": "^0.25.2",
|
||||||
|
"vue": "latest",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
94
apps/client/pages/2fa/index.vue
Normal file
94
apps/client/pages/2fa/index.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<form class="twofa" @submit="onSubmit">
|
||||||
|
<UiButton
|
||||||
|
icon="chevron-left"
|
||||||
|
color="secondary"
|
||||||
|
type="link"
|
||||||
|
class="mb-8"
|
||||||
|
href="/projects"
|
||||||
|
>
|
||||||
|
{{ $t('to_main') }}
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<h1 class="mb-8">
|
||||||
|
Проверка безопасности
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<UiAlert class="mb-16">
|
||||||
|
Введите 6-значный код подтверждения, отправленный на jo**@gmail.com
|
||||||
|
</UiAlert>
|
||||||
|
|
||||||
|
<UiCodeInput id="code">
|
||||||
|
<template #title>
|
||||||
|
<span class="twofa__title"> Код из письма </span>
|
||||||
|
</template>
|
||||||
|
</UiCodeInput>
|
||||||
|
|
||||||
|
<div class="text-align-right mt-8">
|
||||||
|
<UiButton v-if="!showTimer" type="link" @click="resetTimer">
|
||||||
|
Отправить код еще раз
|
||||||
|
</UiButton>
|
||||||
|
<div v-else class="twofa__timer">
|
||||||
|
<span>Отправить код повторно через 0:{{ seconds }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiButton class="twofa__submit" size="large" native-type="submit">
|
||||||
|
{{ $t('next') }}
|
||||||
|
</UiButton>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['auth'],
|
||||||
|
centerContent: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const seconds = ref(30)
|
||||||
|
const showTimer = ref(false)
|
||||||
|
|
||||||
|
const { handleSubmit, isSubmitting } = useForm()
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (values) => {
|
||||||
|
console.log(values)
|
||||||
|
navigateTo('/2fa/setup')
|
||||||
|
})
|
||||||
|
|
||||||
|
function resetTimer() {
|
||||||
|
showTimer.value = true
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (seconds.value === 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
showTimer.value = false
|
||||||
|
seconds.value = 30
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
seconds.value--
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.twofa {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
@include txt-m-b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
margin-top: 32px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__timer {
|
||||||
|
@include txt-i;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
186
apps/client/pages/2fa/setup.vue
Normal file
186
apps/client/pages/2fa/setup.vue
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<form
|
||||||
|
class="twofa-setup"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<UiButton
|
||||||
|
icon="chevron-left"
|
||||||
|
color="secondary"
|
||||||
|
type="link"
|
||||||
|
class="mb-8"
|
||||||
|
:href="step === 0 ? '/projects' : ''"
|
||||||
|
@click="step--"
|
||||||
|
>
|
||||||
|
{{ step === 0 ? $t('to_main') : $t('back') }}
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<h1 class="twofa-setup__title">
|
||||||
|
Настройка двухфакторной аутентификации (2FA)
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<Stepper
|
||||||
|
class="mb-40"
|
||||||
|
:step="step"
|
||||||
|
:items="['Установка', 'Настройка', 'Ввод кода']"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<div v-if="step === 0">
|
||||||
|
<UiAlert class="mb-47">
|
||||||
|
Установите приложение Google Authenticator или другое из списка, чтобы
|
||||||
|
получить коды двухфакторной аутентификации.
|
||||||
|
</UiAlert>
|
||||||
|
|
||||||
|
<div class="twofa-setup__app-block">
|
||||||
|
<p class="mb-8">
|
||||||
|
Google Authenticator app
|
||||||
|
</p>
|
||||||
|
<div class="mb-24">
|
||||||
|
<UiButton
|
||||||
|
color="secondary"
|
||||||
|
type="outlined"
|
||||||
|
class="mr-24"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="d-flex"
|
||||||
|
src="/GetOnAppStore.svg"
|
||||||
|
alt="GetOnAppStore"
|
||||||
|
>
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
color="secondary"
|
||||||
|
type="outlined"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="d-flex"
|
||||||
|
src="/GetOnGooglePlay.svg"
|
||||||
|
alt="GetOnGooglePlay"
|
||||||
|
>
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
Посмотреть весь список 2FA приложений -
|
||||||
|
<UiButton
|
||||||
|
type="link"
|
||||||
|
href="https://www.pcmag.com/picks/the-best-authenticator-apps"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
подробнее
|
||||||
|
</UiButton>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
class="twofa-setup__submit mt-47"
|
||||||
|
size="large"
|
||||||
|
@click="step++"
|
||||||
|
>
|
||||||
|
{{ $t('next') }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="step === 1">
|
||||||
|
<UiAlert class="mb-24">
|
||||||
|
В приложении
|
||||||
|
<strong>двухфакторной аутентификации</strong> отсканируйте QR-код или
|
||||||
|
скопируйте и вставьте <strong>ключ настройки</strong> с этого экрана
|
||||||
|
вручную.
|
||||||
|
</UiAlert>
|
||||||
|
|
||||||
|
<div class="twofa-setup__qrzone">
|
||||||
|
<img src="/QRCode.svg" alt="QRCode" class="mr-40">
|
||||||
|
|
||||||
|
<UiInput
|
||||||
|
id="ssa"
|
||||||
|
model-value="GB5AY4JWLUUF2NRT"
|
||||||
|
readonly
|
||||||
|
copyable
|
||||||
|
label="Ключ настройки"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
class="twofa-setup__submit mt-32"
|
||||||
|
size="large"
|
||||||
|
@click="step++"
|
||||||
|
>
|
||||||
|
{{ $t('next') }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="step === 2">
|
||||||
|
<UiAlert class="mb-50">
|
||||||
|
Далее введите на этом экране <strong>шестизначный код</strong> из
|
||||||
|
приложения двухфакторной аутентификации
|
||||||
|
</UiAlert>
|
||||||
|
|
||||||
|
<UiCodeInput
|
||||||
|
id="pincode"
|
||||||
|
class="mb-21"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<span class="twofa-setup__pin-title"> 2FA-код </span>
|
||||||
|
</template>
|
||||||
|
</UiCodeInput>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
class="twofa-setup__submit mt-32"
|
||||||
|
size="large"
|
||||||
|
native-type="submit"
|
||||||
|
>
|
||||||
|
{{ $t('next') }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['auth'],
|
||||||
|
centerContent: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { handleSubmit, isSubmitting } = useForm()
|
||||||
|
const step = ref(0)
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (values) => {
|
||||||
|
console.log(values)
|
||||||
|
navigateTo('/projects')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.twofa-setup {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
&__qrzone {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__app-block {
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin-bottom: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pin-title {
|
||||||
|
@include txt-m-b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
//margin-top: 47px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
833
apps/client/pages/_.vue
Normal file
833
apps/client/pages/_.vue
Normal file
@@ -0,0 +1,833 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="root"
|
||||||
|
class="root"
|
||||||
|
>
|
||||||
|
<!-- UiButton -->
|
||||||
|
<div class="doc-block">
|
||||||
|
<strong>UiButton</strong>
|
||||||
|
<h1>Кнопки и ссылки</h1>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 16px">
|
||||||
|
<template
|
||||||
|
v-for="type in ['filled', 'outlined', 'ghost', 'link']"
|
||||||
|
:key="type"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="color in ['primary', 'secondary']"
|
||||||
|
:key="color"
|
||||||
|
class="buttons-rows"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="size in ['large', 'medium', 'small']"
|
||||||
|
:key="size"
|
||||||
|
class="buttons-grid"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(props, index) in [
|
||||||
|
{
|
||||||
|
bind: { type, size, color },
|
||||||
|
icon: size === 'large' ? 'plus' : 's-plus',
|
||||||
|
chevron:
|
||||||
|
size === 'large' ? 'chevron-down' : 's-chevron-down',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<UiButton v-bind="props.bind">
|
||||||
|
Button
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
v-bind="props.bind"
|
||||||
|
:icon="props.icon"
|
||||||
|
>
|
||||||
|
Button
|
||||||
|
</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-bind="props.bind"
|
||||||
|
:right-icon="props.icon"
|
||||||
|
>
|
||||||
|
Button
|
||||||
|
</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-bind="props.bind"
|
||||||
|
:left-icon="props.icon"
|
||||||
|
:right-icon="props.chevron"
|
||||||
|
>
|
||||||
|
Button
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
v-bind="props.bind"
|
||||||
|
loading
|
||||||
|
>
|
||||||
|
Button
|
||||||
|
</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-bind="props.bind"
|
||||||
|
:icon="props.icon"
|
||||||
|
/>
|
||||||
|
<UiButton
|
||||||
|
v-bind="props.bind"
|
||||||
|
:left-icon="props.icon"
|
||||||
|
:right-icon="props.chevron"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
v-bind="props.bind"
|
||||||
|
:icon="props.icon"
|
||||||
|
loading
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UiSelect -->
|
||||||
|
<div class="doc-block">
|
||||||
|
<strong>UiSelect</strong>
|
||||||
|
<h1>Select</h1>
|
||||||
|
|
||||||
|
<div class="two-columns">
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Default
|
||||||
|
</h4>
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Multiple
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(multiple, index) in [false, true]"
|
||||||
|
:key="index"
|
||||||
|
class="bg-clr-grey-100 p-8"
|
||||||
|
style="display: inline-flex; gap: 8px; justify-self: center"
|
||||||
|
>
|
||||||
|
<UiSelect
|
||||||
|
:id="!multiple ? 'select' : 'select2'"
|
||||||
|
label="Select"
|
||||||
|
:options="selectOptions"
|
||||||
|
:multiple="multiple"
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
:id="!multiple ? 'select' : 'select2'"
|
||||||
|
label="Searchable"
|
||||||
|
:options="selectOptions"
|
||||||
|
searchable
|
||||||
|
:multiple="multiple"
|
||||||
|
/>
|
||||||
|
<UiSelect
|
||||||
|
:id="!multiple ? 'select' : 'select2'"
|
||||||
|
label="Clearable"
|
||||||
|
:options="selectOptions"
|
||||||
|
clearable
|
||||||
|
:multiple="multiple"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UiDropdown -->
|
||||||
|
<div class="doc-block half">
|
||||||
|
<strong>UiDropdown</strong>
|
||||||
|
<h1>Dropdown \ Выпадающий список</h1>
|
||||||
|
|
||||||
|
<UiDropdown>
|
||||||
|
<UiButton
|
||||||
|
right-icon="s-chevron-down"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
Trigger
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<template #dropdown>
|
||||||
|
<UiDropdownItem
|
||||||
|
v-for="i in 5"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
Item {{ i }}
|
||||||
|
</UiDropdownItem>
|
||||||
|
</template>
|
||||||
|
</UiDropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UiCalendar -->
|
||||||
|
<div class="doc-block half">
|
||||||
|
<strong>UiCalendar</strong>
|
||||||
|
<h1>Calendar</h1>
|
||||||
|
|
||||||
|
<div class="d-inline-block bg-clr-grey-100 p-8">
|
||||||
|
<UiCalendar style="border-radius: 12px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UiInput -->
|
||||||
|
<div class="doc-block half">
|
||||||
|
<strong>UiInput</strong>
|
||||||
|
<h1>Поля ввода</h1>
|
||||||
|
|
||||||
|
<div class="two-columns">
|
||||||
|
<!-- <template v-for="state in []"></template> -->
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Normal
|
||||||
|
</h4>
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Disabled
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<UiInput
|
||||||
|
id="input1"
|
||||||
|
label="Basic"
|
||||||
|
/>
|
||||||
|
<UiInput
|
||||||
|
id="input1"
|
||||||
|
label="Basic"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiInput
|
||||||
|
id="input2"
|
||||||
|
label="With caption"
|
||||||
|
caption="Caption"
|
||||||
|
/>
|
||||||
|
<UiInput
|
||||||
|
id="input2"
|
||||||
|
label="With caption"
|
||||||
|
caption="Caption"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiInput
|
||||||
|
id="input3"
|
||||||
|
label="Clearable"
|
||||||
|
clearable
|
||||||
|
model-value="Value"
|
||||||
|
/>
|
||||||
|
<UiInput
|
||||||
|
id="input3"
|
||||||
|
label="Clearable"
|
||||||
|
clearable
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiInput
|
||||||
|
id="input4"
|
||||||
|
label="Copyable"
|
||||||
|
copyable
|
||||||
|
model-value="Value"
|
||||||
|
/>
|
||||||
|
<UiInput
|
||||||
|
id="input4"
|
||||||
|
label="Copyable"
|
||||||
|
copyable
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiInput
|
||||||
|
id="input5"
|
||||||
|
label="Password"
|
||||||
|
native-type="password"
|
||||||
|
model-value="superPassword"
|
||||||
|
/>
|
||||||
|
<UiInput
|
||||||
|
id="input5"
|
||||||
|
label="Password"
|
||||||
|
native-type="password"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UiSearch -->
|
||||||
|
<div class="doc-block half">
|
||||||
|
<strong>UiSearch</strong>
|
||||||
|
<h1>Поиск</h1>
|
||||||
|
|
||||||
|
<div class="two-columns">
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Normal
|
||||||
|
</h4>
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Disabled
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<UiSearch
|
||||||
|
v-model="searchTerm"
|
||||||
|
size="large"
|
||||||
|
label="Large"
|
||||||
|
/>
|
||||||
|
<UiSearch
|
||||||
|
v-model="searchTerm"
|
||||||
|
size="large"
|
||||||
|
label="Large"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiSearch
|
||||||
|
v-model="searchTerm"
|
||||||
|
size="medium"
|
||||||
|
label="Medium"
|
||||||
|
/>
|
||||||
|
<UiSearch
|
||||||
|
v-model="searchTerm"
|
||||||
|
size="medium"
|
||||||
|
label="Medium"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiSearch
|
||||||
|
v-model="searchTerm"
|
||||||
|
size="small"
|
||||||
|
label="Small"
|
||||||
|
/>
|
||||||
|
<UiSearch
|
||||||
|
v-model="searchTerm"
|
||||||
|
size="small"
|
||||||
|
label="Small"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UiSwitch -->
|
||||||
|
<div class="doc-block third">
|
||||||
|
<strong>UiSwitch</strong>
|
||||||
|
<h1>Switch</h1>
|
||||||
|
|
||||||
|
<div class="two-columns justify-items-center">
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Normal
|
||||||
|
</h4>
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Disabled
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<UiSwitch id="switch" />
|
||||||
|
<UiSwitch
|
||||||
|
id="switch"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiSwitch
|
||||||
|
id="switch2"
|
||||||
|
:loading="switchLoading"
|
||||||
|
:before-change="beforeChange"
|
||||||
|
/>
|
||||||
|
<UiSwitch
|
||||||
|
id="switch2"
|
||||||
|
:loading="switchLoading"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UiCheckbox -->
|
||||||
|
<div class="doc-block third">
|
||||||
|
<strong>UiCheckbox</strong>
|
||||||
|
<h1>Checkbox</h1>
|
||||||
|
|
||||||
|
<div class="two-columns">
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Normal
|
||||||
|
</h4>
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Disabled
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<UiCheckbox
|
||||||
|
id="checkbox"
|
||||||
|
label="Label"
|
||||||
|
true-value="checkbox1"
|
||||||
|
/>
|
||||||
|
<UiCheckbox
|
||||||
|
id="checkbox"
|
||||||
|
label="Label"
|
||||||
|
true-value="checkbox1"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiCheckbox
|
||||||
|
id="checkbox"
|
||||||
|
true-value="checkbox2"
|
||||||
|
/>
|
||||||
|
<UiCheckbox
|
||||||
|
id="checkbox"
|
||||||
|
true-value="checkbox2"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UiRadio -->
|
||||||
|
<div class="doc-block third">
|
||||||
|
<strong>UiRadio</strong>
|
||||||
|
<h1>Radio</h1>
|
||||||
|
|
||||||
|
<div class="two-columns">
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Normal
|
||||||
|
</h4>
|
||||||
|
<h4 class="group-heading">
|
||||||
|
Disabled
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<UiRadio
|
||||||
|
id="radio"
|
||||||
|
label="Label"
|
||||||
|
value="radio1"
|
||||||
|
/>
|
||||||
|
<UiRadio
|
||||||
|
id="radio"
|
||||||
|
label="Label"
|
||||||
|
value="radio1"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiRadio
|
||||||
|
id="radio"
|
||||||
|
value="radio2"
|
||||||
|
/>
|
||||||
|
<UiRadio
|
||||||
|
id="radio"
|
||||||
|
value="radio2"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UiAccordion -->
|
||||||
|
<div class="doc-block">
|
||||||
|
<strong>UiAccordion</strong>
|
||||||
|
<h1>Спойлер \ Аккордеон</h1>
|
||||||
|
|
||||||
|
<div style="width: 600px; margin: 0 auto">
|
||||||
|
<UiAccordion>
|
||||||
|
<UiAccordionItem title="Title">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ad
|
||||||
|
commodi, culpa delectus deleniti doloribus ducimus eaque eos eveniet
|
||||||
|
expedita harum, in incidunt itaque magnam, nulla quam ratione
|
||||||
|
recusandae reprehenderit veniam?
|
||||||
|
</UiAccordionItem>
|
||||||
|
|
||||||
|
<UiAccordionItem title="Title 2">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolor
|
||||||
|
exercitationem iusto obcaecati placeat sunt! Dicta dolorem ducimus
|
||||||
|
minima molestias obcaecati odio omnis ratione recusandae,
|
||||||
|
voluptatum? Aspernatur autem commodi praesentium sint? Lorem ipsum
|
||||||
|
dolor sit amet, consectetur adipisicing elit. Expedita optio, vitae.
|
||||||
|
Aut consectetur cum dolores est exercitationem facilis fugiat
|
||||||
|
incidunt iste maiores modi nobis quia rerum, sit vel veniam vero!
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolor
|
||||||
|
exercitationem iusto obcaecati placeat sunt! Dicta dolorem ducimus
|
||||||
|
minima molestias obcaecati odio omnis ratione recusandae,
|
||||||
|
voluptatum? Aspernatur autem commodi praesentium sint? Lorem ipsum
|
||||||
|
dolor sit amet, consectetur adipisicing elit. Expedita optio, vitae.
|
||||||
|
Aut consectetur cum dolores est exercitationem facilis fugiat
|
||||||
|
incidunt iste maiores modi nobis quia rerum, sit vel veniam vero!
|
||||||
|
</UiAccordionItem>
|
||||||
|
|
||||||
|
<UiAccordionItem title="Title 3">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolor
|
||||||
|
exercitationem iusto obcaecati placeat sunt! Dicta dolorem ducimus
|
||||||
|
minima molestias obcaecati odio omnis ratione recusandae,
|
||||||
|
voluptatum? Aspernatur autem commodi praesentium sint? Lorem ipsum
|
||||||
|
dolor sit amet, consectetur adipisicing elit. Expedita optio, vitae.
|
||||||
|
Aut consectetur cum dolores est exercitationem facilis fugiat
|
||||||
|
incidunt iste maiores modi nobis quia rerum, sit vel veniam vero!
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolor
|
||||||
|
exercitationem iusto obcaecati placeat sunt! Dicta dolorem ducimus
|
||||||
|
minima molestias obcaecati odio omnis ratione recusandae,
|
||||||
|
voluptatum? Aspernatur autem commodi praesentium sint? Lorem ipsum
|
||||||
|
dolor sit amet, consectetur adipisicing elit. Expedita optio, vitae.
|
||||||
|
Aut consectetur cum dolores est exercitationem facilis fugiat
|
||||||
|
incidunt iste maiores modi nobis quia rerum, sit vel veniam vero!
|
||||||
|
</UiAccordionItem>
|
||||||
|
</UiAccordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UiAlert -->
|
||||||
|
<div class="doc-block">
|
||||||
|
<strong>UiAlert</strong>
|
||||||
|
<h1>Алерты</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="three-columns"
|
||||||
|
style="justify-items: center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-100"
|
||||||
|
style="max-width: 400px"
|
||||||
|
>
|
||||||
|
<UiAlert
|
||||||
|
v-for="type in [
|
||||||
|
'neutral',
|
||||||
|
'positive',
|
||||||
|
'warning',
|
||||||
|
'negative',
|
||||||
|
'marketing',
|
||||||
|
]"
|
||||||
|
:key="type"
|
||||||
|
class="my-8"
|
||||||
|
:type="type"
|
||||||
|
text="Текст сообщения"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="w-100"
|
||||||
|
style="max-width: 400px"
|
||||||
|
>
|
||||||
|
<UiAlert
|
||||||
|
v-for="type in [
|
||||||
|
'neutral',
|
||||||
|
'positive',
|
||||||
|
'warning',
|
||||||
|
'negative',
|
||||||
|
'marketing',
|
||||||
|
]"
|
||||||
|
:key="type"
|
||||||
|
class="my-8"
|
||||||
|
:type="type"
|
||||||
|
title="Заголовок"
|
||||||
|
text="Текст сообщения"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="w-100"
|
||||||
|
style="max-width: 400px"
|
||||||
|
>
|
||||||
|
<UiAlert
|
||||||
|
v-for="type in [
|
||||||
|
'neutral',
|
||||||
|
'positive',
|
||||||
|
'warning',
|
||||||
|
'negative',
|
||||||
|
'marketing',
|
||||||
|
]"
|
||||||
|
:key="type"
|
||||||
|
:type="type"
|
||||||
|
class="my-8"
|
||||||
|
title="Заголовок"
|
||||||
|
text="Текст сообщения"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<UiButton type="link">
|
||||||
|
Действие
|
||||||
|
</UiButton>
|
||||||
|
</template>
|
||||||
|
</UiAlert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UiBadge -->
|
||||||
|
<div class="doc-block">
|
||||||
|
<strong>UiBadge</strong>
|
||||||
|
<h1>Бейджы</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="three-columns"
|
||||||
|
style="justify-items: center"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<UiBadge
|
||||||
|
v-for="type in [
|
||||||
|
'neutral',
|
||||||
|
'positive',
|
||||||
|
'warning',
|
||||||
|
'negative',
|
||||||
|
'marketing',
|
||||||
|
]"
|
||||||
|
:key="type"
|
||||||
|
class="my-8"
|
||||||
|
:type="type"
|
||||||
|
text="Текст бейджа"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<UiBadge
|
||||||
|
v-for="type in [
|
||||||
|
'neutral',
|
||||||
|
'positive',
|
||||||
|
'warning',
|
||||||
|
'negative',
|
||||||
|
'marketing',
|
||||||
|
]"
|
||||||
|
:key="type"
|
||||||
|
class="my-8"
|
||||||
|
:type="type"
|
||||||
|
text="Текст бейджа"
|
||||||
|
:icon="randomIcon()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<UiBadge
|
||||||
|
v-for="type in [
|
||||||
|
'neutral',
|
||||||
|
'positive',
|
||||||
|
'warning',
|
||||||
|
'negative',
|
||||||
|
'marketing',
|
||||||
|
]"
|
||||||
|
:key="type"
|
||||||
|
:type="type"
|
||||||
|
class="my-8"
|
||||||
|
text="Текст бейджа"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<i :class="`icon-${randomIcon()}`" />
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<i :class="`icon-${randomIcon()}`" />
|
||||||
|
</template>
|
||||||
|
</UiBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notification -->
|
||||||
|
<div class="doc-block">
|
||||||
|
<strong>useNotify \ $notify</strong>
|
||||||
|
<h1>Уведомления</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 150px);
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<UiButton @click="randomNotification('top-left')">
|
||||||
|
Top left
|
||||||
|
</UiButton>
|
||||||
|
<UiButton @click="randomNotification('top-right')">
|
||||||
|
Top Right
|
||||||
|
</UiButton>
|
||||||
|
<UiButton @click="randomNotification('bottom-left')">
|
||||||
|
Bottom left
|
||||||
|
</UiButton>
|
||||||
|
<UiButton @click="randomNotification('bottom-right')">
|
||||||
|
Bottom Right
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CopyButton -->
|
||||||
|
<div class="doc-block">
|
||||||
|
<strong>UiCopyButton</strong>
|
||||||
|
<h1>Кнопка копирования</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, 150px);
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<UiCopyButton
|
||||||
|
title="Payment link"
|
||||||
|
pressed-title="Link copied"
|
||||||
|
value="Test value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Icons -->
|
||||||
|
<!-- <div -->
|
||||||
|
<!-- v-for="(font, index) in [icons, smallIcons]" -->
|
||||||
|
<!-- :key="font.metadata.name" -->
|
||||||
|
<!-- class="doc-block half" -->
|
||||||
|
<!-- > -->
|
||||||
|
<!-- <strong> -->
|
||||||
|
<!-- {{ _ === 0 ? '<i class="icon-*" />' : '<i class="icon-s-*" />' }} -->
|
||||||
|
<!-- </strong> -->
|
||||||
|
<!-- <h1>Иконки - {{ font.metadata.name }}</h1> -->
|
||||||
|
|
||||||
|
<!-- <div> -->
|
||||||
|
<!-- <div -->
|
||||||
|
<!-- v-for="(iconSet, index) in font.iconSets" -->
|
||||||
|
<!-- :key="_" -->
|
||||||
|
<!-- class="icons-grid" -->
|
||||||
|
<!-- > -->
|
||||||
|
<!-- <div -->
|
||||||
|
<!-- v-for="icon in iconSet.selection" -->
|
||||||
|
<!-- :key="icon.name" -->
|
||||||
|
<!-- class="preview-icon" -->
|
||||||
|
<!-- @click="copy(icon.name)" -->
|
||||||
|
<!-- > -->
|
||||||
|
<!-- <i :class="`${font.preferences.fontPref.prefix}${icon.name}`" /> -->
|
||||||
|
<!-- <span>{{ icon.name }}</span> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { ALERT_TYPES } from 'ui-layer/components/alert/types'
|
||||||
|
import type { NotificationPlacement } from 'ui-layer/components/notification/types'
|
||||||
|
import { sample } from 'lodash-es'
|
||||||
|
import icons from '#build/ui/available-icons'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'empty',
|
||||||
|
dev: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const searchTerm = ref<string>()
|
||||||
|
const switchLoading = ref(false)
|
||||||
|
|
||||||
|
useForm()
|
||||||
|
|
||||||
|
function copy(value: string) {
|
||||||
|
navigator.clipboard.writeText(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function beforeChange() {
|
||||||
|
switchLoading.value = true
|
||||||
|
|
||||||
|
const result = await new Promise(resolve =>
|
||||||
|
setTimeout(() => resolve(true), 1000),
|
||||||
|
)
|
||||||
|
|
||||||
|
switchLoading.value = false
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectOptions = computed(() => {
|
||||||
|
return Array.from(Array(300).keys()).map(i => `Option ${i + 1}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
let notificationTypeCounter = 0
|
||||||
|
function randomNotification(placement: NotificationPlacement) {
|
||||||
|
const notify = useNotify()
|
||||||
|
|
||||||
|
notify({
|
||||||
|
placement,
|
||||||
|
text: 'Текст',
|
||||||
|
title: 'Заголовок',
|
||||||
|
type: ALERT_TYPES[notificationTypeCounter],
|
||||||
|
})
|
||||||
|
|
||||||
|
notificationTypeCounter++
|
||||||
|
notificationTypeCounter %= ALERT_TYPES.length
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomIcon() {
|
||||||
|
return sample(icons.filter(icon => icon.startsWith('s-')))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.root {
|
||||||
|
padding: 30px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-block {
|
||||||
|
background-color: $clr-white;
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 40px 60px;
|
||||||
|
grid-column: span 6;
|
||||||
|
|
||||||
|
&.third {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.half {
|
||||||
|
grid-column: span 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
> strong {
|
||||||
|
color: $clr-grey-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
> h1 {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.three-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons-rows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-icon {
|
||||||
|
display: flex;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
width: 85px;
|
||||||
|
height: 65px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: $clr-grey-100;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 0.2s ease-out;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: $clr-grey-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
@include txt-s-m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-heading {
|
||||||
|
text-align: center;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
146
apps/client/pages/create/invoice/[projectId].vue
Normal file
146
apps/client/pages/create/invoice/[projectId].vue
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<template>
|
||||||
|
<div class="create-invoice">
|
||||||
|
<Transition
|
||||||
|
name="fade"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
|
<PageForm
|
||||||
|
v-if="!invoiceLink"
|
||||||
|
title="Creating invoice"
|
||||||
|
:back-link="`/projects/${$route.params.projectId}/invoices`"
|
||||||
|
:back-text="$t('back_to', [project.name])"
|
||||||
|
:submit-text="$t('create')"
|
||||||
|
:handler="onSubmit"
|
||||||
|
>
|
||||||
|
<UiInput
|
||||||
|
id="amount"
|
||||||
|
label="Invoice amount"
|
||||||
|
rules="required"
|
||||||
|
:mask="{ mask: Number }"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<CurrencyName
|
||||||
|
code="USDT"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UiInput>
|
||||||
|
</PageForm>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="create-invoice__summary"
|
||||||
|
>
|
||||||
|
<UiButton
|
||||||
|
icon="chevron-left"
|
||||||
|
color="secondary"
|
||||||
|
type="link"
|
||||||
|
class="mb-8"
|
||||||
|
:href="`/projects/${$route.params.projectId}/invoices`"
|
||||||
|
>
|
||||||
|
{{ $t('back_to', [project.name]) }}
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<h1 class="mb-32">
|
||||||
|
Invoice created
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<UiAlert
|
||||||
|
type="neutral"
|
||||||
|
class="mb-24"
|
||||||
|
>
|
||||||
|
<span> Creation time</span>
|
||||||
|
{{ dayjs().format('DD.MM.YY HH:mm') }}.
|
||||||
|
<span>It will expires in </span>
|
||||||
|
<strong>30 minutes.</strong>
|
||||||
|
</UiAlert>
|
||||||
|
|
||||||
|
<UiInput
|
||||||
|
id="invoice_link"
|
||||||
|
copyable
|
||||||
|
inputmode="none"
|
||||||
|
label="Ссылка на счет"
|
||||||
|
:model-value="invoiceLink"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
size="large"
|
||||||
|
class="w-100 mt-24"
|
||||||
|
:href="`/projects/${route.params.projectId}/invoices`"
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['auth'],
|
||||||
|
centerContent: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const { data: project } = await useAsyncData(
|
||||||
|
'project',
|
||||||
|
() =>
|
||||||
|
$api(`/online_stores/${route.params.projectId}`, {
|
||||||
|
method: 'get',
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!project.value) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: 'Page Not Found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const notify = useNotify()
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
|
||||||
|
const invoiceLink = ref('')
|
||||||
|
|
||||||
|
async function onSubmit(values) {
|
||||||
|
try {
|
||||||
|
const result = await $api('/invoices', {
|
||||||
|
method: 'post',
|
||||||
|
body: {
|
||||||
|
...values,
|
||||||
|
currencyCode: 'USDT',
|
||||||
|
onlineStoreId: +route.params.projectId,
|
||||||
|
orderId: uuidv4(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
invoiceLink.value = `${runtimeConfig.public.payHost}/${result.invoiceId}`
|
||||||
|
|
||||||
|
notify({
|
||||||
|
type: 'positive',
|
||||||
|
text: 'Инвойс успешно создан',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
setStaticError({
|
||||||
|
status: e.status,
|
||||||
|
message: 'Something went wrong',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.create-invoice {
|
||||||
|
&__summary {
|
||||||
|
width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
451
apps/client/pages/create/withdraw/[projectId]/[assetCode].vue
Normal file
451
apps/client/pages/create/withdraw/[projectId]/[assetCode].vue
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
<template>
|
||||||
|
<form
|
||||||
|
class="create-withdraw"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<Transition
|
||||||
|
name="shift"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!showSummary"
|
||||||
|
class="withdraw-form"
|
||||||
|
title="Withdrawal request"
|
||||||
|
>
|
||||||
|
<FormHeader
|
||||||
|
class="withdraw-form__header"
|
||||||
|
title="Withdrawal request"
|
||||||
|
:back-link="`/projects/${$route.params.projectId}`"
|
||||||
|
:back-text="$t('back_to', [project.name])"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-16">
|
||||||
|
<CurrencyName
|
||||||
|
:code="route.params.assetCode"
|
||||||
|
name="Tether"
|
||||||
|
class="create-withdraw__currency-name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p class="create-withdraw__balance">
|
||||||
|
{{ balance }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="p-16 bg-clr-grey-200"
|
||||||
|
style="border-radius: 16px"
|
||||||
|
>
|
||||||
|
<UiInput
|
||||||
|
id="paymentAddress"
|
||||||
|
rules="required"
|
||||||
|
label="Withdrawal wallet"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NetworkSelect
|
||||||
|
id="network"
|
||||||
|
:asset-code="route.params.assetCode"
|
||||||
|
model-value="TRC20"
|
||||||
|
class="mt-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mt-16 p-16 bg-clr-grey-200"
|
||||||
|
style="border-radius: 16px"
|
||||||
|
>
|
||||||
|
<UiInput
|
||||||
|
id="amount"
|
||||||
|
rules="required"
|
||||||
|
:label="`Минимальная сумма ${minAmount} ${route.params.assetCode}`"
|
||||||
|
:mask="{
|
||||||
|
mask: Number,
|
||||||
|
min: minAmount,
|
||||||
|
max: maxAmount,
|
||||||
|
scale: exponent,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<UiButton
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="setValues({ amount: maxAmount }, false)"
|
||||||
|
>
|
||||||
|
{{ $t('maximum') }}
|
||||||
|
</UiButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #caption>
|
||||||
|
<div class="create-withdraw__limit">
|
||||||
|
<span>Доступный лимит на вывод - 1000 USDT</span>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
class="create-withdraw__upgrade"
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
href="/verification"
|
||||||
|
>
|
||||||
|
Повысить лимит
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UiInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="withdraw-form__bottom">
|
||||||
|
<div class="withdraw-amount withdraw-amount--fee">
|
||||||
|
<div class="withdraw-amount__title">
|
||||||
|
{{ $t('fee') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="withdraw-amount__value">
|
||||||
|
{{ fee }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="withdraw-amount">
|
||||||
|
<div class="withdraw-amount__title">
|
||||||
|
К отправке
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="withdraw-amount__value">
|
||||||
|
{{ total }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
size="large"
|
||||||
|
class="create-withdraw__continue"
|
||||||
|
native-type="submit"
|
||||||
|
:disabled="balance <= 0"
|
||||||
|
>
|
||||||
|
{{ $t('continue') }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="withdraw-summary"
|
||||||
|
>
|
||||||
|
<FormHeader
|
||||||
|
class="withdraw-summary__header"
|
||||||
|
title="Confirm the withdrawal"
|
||||||
|
:back-link="`/projects/${$route.params.projectId}`"
|
||||||
|
:back-text="$t('back_to', [project.name])"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<dl class="withdraw-summary__details withdraw-details">
|
||||||
|
<dt>Withdrawal wallet</dt>
|
||||||
|
<dd>
|
||||||
|
<TextShortener :text="values.paymentAddress" />
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Network</dt>
|
||||||
|
<dd class="text-clr-green-500">
|
||||||
|
<CurrencyName
|
||||||
|
:code="route.params.assetCode"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>{{ $t('fee') }}</dt>
|
||||||
|
<dd>{{ fee }}</dd>
|
||||||
|
|
||||||
|
<dt>К отправке</dt>
|
||||||
|
<dd class="withdraw-details__amount">
|
||||||
|
{{ total }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<UiAlert class="withdraw-summary__alert">
|
||||||
|
Пожалуйста, подтвердите, что операцию инициировали именно вы.
|
||||||
|
</UiAlert>
|
||||||
|
|
||||||
|
<UiCodeInput
|
||||||
|
id="pincode"
|
||||||
|
class="withdraw-summary__pincode"
|
||||||
|
title="2FA-код"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="withdraw-summary__actions">
|
||||||
|
<UiButton
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
@click="showSummary = false"
|
||||||
|
>
|
||||||
|
{{ $t('back') }}
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
size="large"
|
||||||
|
native-type="submit"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
>
|
||||||
|
{{ $t('withdraw') }}
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { computedAsync } from '@vueuse/core'
|
||||||
|
import Decimal from 'decimal.js'
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['auth'],
|
||||||
|
centerContent: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const { data: project } = await useAsyncData(
|
||||||
|
'project',
|
||||||
|
() => {
|
||||||
|
return $api(`/online_stores/${route.params.projectId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!project.value) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: 'Page Not Found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const notify = useNotify()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { data: tariff } = await useAsyncData('tariff', () => {
|
||||||
|
return $api(`/tariffs/current?currency=${route.params.assetCode}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: account } = await useAsyncData('account', () =>
|
||||||
|
$api(`/accounts/${project.value.accountIds[0]}`, {
|
||||||
|
method: 'GET',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const showSummary = ref(false)
|
||||||
|
|
||||||
|
const { isSubmitting, values, setValues, handleSubmit } = useForm({ keepValuesOnUnmount: true })
|
||||||
|
|
||||||
|
const commission = computedAsync(async () => {
|
||||||
|
if (values.amount) {
|
||||||
|
try {
|
||||||
|
const result = await $api(`/withdrawals/commission`, { method: 'GET', params: { amount: values.amount, currency: route.params.assetCode } })
|
||||||
|
|
||||||
|
return result?.commission ?? 0
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}, 0, { lazy: true })
|
||||||
|
const fee = computed(() => $money.fullFormat(commission.value, route.params.assetCode))
|
||||||
|
const balance = computed(() => $money.format(account.value.balance, route.params.assetCode))
|
||||||
|
const maxAmount = computed(() => Math.min(1000, account.value.balance))
|
||||||
|
const minAmount = computed(() => tariff.value?.data.minAmount)
|
||||||
|
const exponent = computed(() => $money.getExponent(route.params.assetCode))
|
||||||
|
const total = computed(() => $money.fullFormat(Decimal.sub(values.amount || 0, commission.value), route.params.assetCode))
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (values) => {
|
||||||
|
if (!showSummary.value) {
|
||||||
|
showSummary.value = true
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.pincode !== '123123') {
|
||||||
|
notify({
|
||||||
|
id: 'withdraw_error',
|
||||||
|
text: t('invalid_otp_code'),
|
||||||
|
type: 'negative',
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// const idempotencyKey = `${route.params.projectId + values.paymentAddress + route.params.assetCode}TRON${values.amount}`
|
||||||
|
|
||||||
|
await $api('/withdrawals', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
...values,
|
||||||
|
idempotencyKey: uuidv4().substring(0, 20),
|
||||||
|
amount: +values.amount,
|
||||||
|
onlineStoreId: +route.params.projectId,
|
||||||
|
currencyCode: route.params.assetCode,
|
||||||
|
blockchainCode: 'TRON',
|
||||||
|
saveWallet: false,
|
||||||
|
pincode: undefined,
|
||||||
|
network: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
notify({
|
||||||
|
id: 'withdraw_success',
|
||||||
|
type: 'positive',
|
||||||
|
text: 'The withdrawal request has been successfully processed',
|
||||||
|
})
|
||||||
|
|
||||||
|
navigateTo(`/projects/${route.params.projectId}`)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
notify({
|
||||||
|
id: 'withdraw_error',
|
||||||
|
text: t('something_went_wrong'),
|
||||||
|
type: 'negative',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.create-withdraw {
|
||||||
|
&__currency-name {
|
||||||
|
@include txt-l-sb('currency-name', true);
|
||||||
|
@include txt-r-m('currency-name-code', true);
|
||||||
|
|
||||||
|
--currency-name-gap: 2px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__balance {
|
||||||
|
@include txt-l-sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__continue {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__limit {
|
||||||
|
@include txt-r-m;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.withdraw-form,
|
||||||
|
.withdraw-summary {
|
||||||
|
width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.withdraw-form {
|
||||||
|
&__bottom {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.withdraw-summary {
|
||||||
|
&__details {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__alert {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pincode {
|
||||||
|
padding-block: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 32px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.withdraw-amount {
|
||||||
|
$self: &;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
height: 48px;
|
||||||
|
flex: 0 0 146px;
|
||||||
|
|
||||||
|
&--fee {
|
||||||
|
flex: 0 0 100px
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
@include txt-r-b;
|
||||||
|
|
||||||
|
color: $clr-grey-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
@include txt-l-sb;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
#{$self}--fee & {
|
||||||
|
@include txt-i-sb;
|
||||||
|
|
||||||
|
color: $clr-grey-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& + & {
|
||||||
|
border-left: 1px solid $clr-grey-300;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.withdraw-details {
|
||||||
|
@include txt-i-m;
|
||||||
|
@include txt-i-m('text-shortener', true);
|
||||||
|
@include txt-i-m('currency-name-code', true);
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: $clr-grey-200;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
row-gap: 16px;
|
||||||
|
color: $clr-grey-500;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
dt, dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
color: $clr-grey-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__amount {
|
||||||
|
@include txt-l-sb;
|
||||||
|
|
||||||
|
color: $clr-black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user