diff --git a/.commitlintrc.mjs b/.commitlintrc.mjs new file mode 100644 index 0000000..e3610d9 --- /dev/null +++ b/.commitlintrc.mjs @@ -0,0 +1,30 @@ +const config = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [ + 2, + 'always', + [ + 'build', + 'chore', + 'config', + 'doc', + 'feat', + 'fix', + 'hotfix', + 'i18n', + 'refactor', + 'revert', + 'test', + 'ui', + 'wip', + 'publish', + 'docker', + 'WIP', + ], + ], + }, + ignores: [(message) => message.includes('WIP'), (message) => message.includes('wip')], +}; + +export default config; diff --git a/.env.sample b/.env.sample index b6220df..a2e1c7c 100644 --- a/.env.sample +++ b/.env.sample @@ -23,8 +23,6 @@ CONFIG_PATH=./config SSH_PATH=./ssh SSH_HOST=./ssh_host BORG_REPOSITORY_PATH=./repos -TMP_PATH=./tmp -LOGS_PATH=./logs ## Optional variables section ## @@ -32,10 +30,22 @@ LOGS_PATH=./logs FQDN_LAN= SSH_SERVER_PORT_LAN= +# Disable the DELETE feature +#DISABLE_DELETE_REPO=true + +# Disable the integrations (API tokens to CRUD repositories) +#DISABLE_INTEGRATIONS=true + +# Hide the SSH port in the UI : quickcommands & wizard +#HIDE_SSH_PORT=true + # SMTP server settings MAIL_SMTP_FROM= MAIL_SMTP_HOST= MAIL_SMTP_PORT= MAIL_SMTP_LOGIN= MAIL_SMTP_PWD= -MAIL_REJECT_SELFSIGNED_TLS= \ No newline at end of file +MAIL_REJECT_SELFSIGNED_TLS= + +# Force app to start on IPv6 +#HOSTNAME=:: \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..136334e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report a bug +title: '' +labels: '' +assignees: '' + +--- + +**BorgWarehouse version :** +**Installation type :** +- [ ] Docker +- [ ] Baremetal (Debian/Ubuntu) +- [ ] Other environment : + +------- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. + +**Please, [BorgWarehouse's documentation](https://borgwarehouse.com/) + is up to date and comprehensive, so take the time to look for answers. You can also look for answers in the project's historical [github issues](https://github.com/Ravinou/borgwarehouse/issues?q=is%3Aissue%20state%3Aclosed). I take time to answer each issue, but it's always less time for BorgWarehouse development. Thanks in advance.** diff --git a/.github/ISSUE_TEMPLATE/i-need-help.md b/.github/ISSUE_TEMPLATE/i-need-help.md new file mode 100644 index 0000000..8aa78e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/i-need-help.md @@ -0,0 +1,21 @@ +--- +name: I need help +about: You need help about installation, usage, or specific cases. +title: '' +labels: help wanted +assignees: '' + +--- + +**BorgWarehouse version :** +**Installation type :** +- [ ] Docker +- [ ] Baremetal (Debian/Ubuntu) +- [ ] Other environment : + +------- + +Describe your problem here. + +**Please, [BorgWarehouse's documentation](https://borgwarehouse.com/) + is up to date and comprehensive, so take the time to look for answers. You can also look for answers in the project's historical [github issues](https://github.com/Ravinou/borgwarehouse/issues?q=is%3Aissue%20state%3Aclosed). I take time to answer each issue, but it's always less time for BorgWarehouse development. Thanks in advance.** diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3110836..34a7437 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,16 +1,18 @@ version: 2 updates: - - package-ecosystem: "docker" - directory: "/" + - package-ecosystem: 'docker' + directory: '/' schedule: - interval: "daily" - - package-ecosystem: "npm" - directory: "/" + interval: 'daily' + # Note: Dependabot uses "npm" ecosystem but automatically detects pnpm-lock.yaml + # Make sure package-lock.json is gitignored to prevent confusion + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "daily" + interval: 'daily' # Maintain dependencies for GitHub Actions # src: https://github.com/marketplace/actions/build-and-push-docker-images#keep-up-to-date-with-github-dependabot - - package-ecosystem: "github-actions" - directory: "/" + - package-ecosystem: 'github-actions' + directory: '/' schedule: - interval: "daily" + interval: 'daily' diff --git a/.github/workflows/bats.yml b/.github/workflows/bats.yml new file mode 100644 index 0000000..825fd0d --- /dev/null +++ b/.github/workflows/bats.yml @@ -0,0 +1,29 @@ +name: Bats + +permissions: + contents: read + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop +jobs: + bats-test: + name: Run bats tests against shells + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build container & run bats tests + run: | + docker compose -f tests/bats/docker-compose.yml up --abort-on-container-exit --build diff --git a/.github/workflows/docker-image-develop.yml b/.github/workflows/docker-image-develop.yml index 80daaea..14c33a5 100644 --- a/.github/workflows/docker-image-develop.yml +++ b/.github/workflows/docker-image-develop.yml @@ -1,29 +1,38 @@ name: Build and Push Docker Image for Develop Branch on: - push: - branches: - - 'develop' + push: + branches: + - 'develop' + +permissions: + contents: read jobs: - docker: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 # linux/arm/v7 arm32 is not supported by node20 https://github.com/nodejs/docker-node/issues/1946 - tags: borgwarehouse/borgwarehouse:develop + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Update package.json version + run: | + COMMIT=$(git rev-parse --short HEAD) + echo "Current Commit: $COMMIT" + jq '.version = "develop-'$COMMIT'"' package.json > package.tmp.json + mv package.tmp.json package.json + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 # linux/arm/v7 arm32 is not supported by node20 https://github.com/nodejs/docker-node/issues/1946 + tags: borgwarehouse/borgwarehouse:develop diff --git a/.github/workflows/docker-image-latest.yml b/.github/workflows/docker-image-latest.yml index f273b46..7957cb8 100644 --- a/.github/workflows/docker-image-latest.yml +++ b/.github/workflows/docker-image-latest.yml @@ -1,4 +1,6 @@ name: Build and Push Docker Image +permissions: + contents: read on: push: @@ -10,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx @@ -21,7 +23,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . push: true diff --git a/.github/workflows/docker-image-release.yml b/.github/workflows/docker-image-release.yml index 26dd466..01f4eb5 100644 --- a/.github/workflows/docker-image-release.yml +++ b/.github/workflows/docker-image-release.yml @@ -5,12 +5,15 @@ on: types: - published +permissions: + contents: read + jobs: docker: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx @@ -24,7 +27,7 @@ jobs: id: get_release_tag run: echo "::set-output name=TAG::${{ github.event.release.tag_name }}" - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . push: true diff --git a/.github/workflows/docker-image-test.yml b/.github/workflows/docker-image-test.yml index 448f45c..4d716ad 100644 --- a/.github/workflows/docker-image-test.yml +++ b/.github/workflows/docker-image-test.yml @@ -1,21 +1,24 @@ -name: Test Docker Container Build on Pull Request +name: Test to build docker container on Pull Request + +permissions: + contents: read on: - pull_request: - branches: - - main - - develop + pull_request: + branches: + - main + - develop jobs: - build-container: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Build Docker Container - run: | - docker buildx build --platform linux/amd64,linux/arm64 -t borgwarehouse:pr-${{ github.event.pull_request.number }} . + build-container: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build BorgWarehouse Container + run: | + docker buildx build --platform linux/amd64,linux/arm64 -t borgwarehouse:pr-${{ github.event.pull_request.number }} . diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 92522ae..8b954af 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -4,19 +4,21 @@ on: - main - develop pull_request: - branches: main + branches: + - main + - develop -name: "Shellcheck" +name: 'Shellcheck' permissions: {} jobs: shellcheck: name: Shellcheck runs-on: ubuntu-latest - + steps: - - uses: actions/checkout@v4 - + - uses: actions/checkout@v6 + - name: Run ShellCheck uses: ludeeus/action-shellcheck@master env: diff --git a/.github/workflows/vitest.yml b/.github/workflows/vitest.yml new file mode 100644 index 0000000..fe9d512 --- /dev/null +++ b/.github/workflows/vitest.yml @@ -0,0 +1,63 @@ +name: Vitest & ESLint CI + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +permissions: + contents: read + +jobs: + test: + name: Run Vitest + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run Vitest + run: pnpm run test + + lint: + name: Run ESLint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run ESLint + run: pnpm exec eslint diff --git a/.gitignore b/.gitignore index 2373057..c95738d 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,14 @@ typings/ # Optional npm cache directory .npm +# pnpm +.pnpm-store/ +pnpm-debug.log* + +# Lock files (pnpm-lock.yaml is used) +package-lock.json +yarn.lock + # Optional eslint cache .eslintcache @@ -111,4 +119,7 @@ config/repo.json config/users.json # docker files -docker-compose.yml \ No newline at end of file +docker-compose.yml + +# Commit tests docker-compose +!tests/bats/docker-compose.yml \ No newline at end of file diff --git a/.husky/append-icon.sh b/.husky/append-icon.sh new file mode 100755 index 0000000..a54594a --- /dev/null +++ b/.husky/append-icon.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +# define log prefix +prefix="pre-commit:" + +# store message file, first and only param of hook +commitMessageFile="$1" + +# breaking change icon ! +boomIcon=':boom:' + +# check for breaking change in file content +# find any line starting with 'BREAKING CHANGE' +function checkBreakingChangeInBody() { + breakingChange='BREAKING CHANGE' + while read -r line; do + if [[ "$line" == "$breakingChange"* ]]; then + echo "$prefix found $breakingChange in message body" + return 0 + fi + done < "$1" + return 1 +} + +function findTypeIcon() { + message="$1" + + if [[ "$message" =~ ^.*!:\ .* ]]; then + echo "$boomIcon" + return 0 + fi + + declare -A icons=( + [build]='๐Ÿค–' + [chore]='๐Ÿงน' + ["chore(deps)"]='๐Ÿงน' + [config]='๐Ÿ”ง' + [deploy]='๐Ÿš€' + [doc]='๐Ÿ“š' + [feat]='โœจ' + [fix]='๐Ÿ›' + [hotfix]='๐Ÿš‘' + [i18n]='๐Ÿ’ฌ' + [publish]='๐Ÿ“ฆ' + [refactor]='โšก' + [revert]='โช' + [test]='โœ…' + [ui]='๐ŸŽจ' + [wip]='๐Ÿšง' + [WIP]='๐Ÿšง' + [docker]='๐Ÿณ' + ) + + commit_type="${message%%:*}" + + icon="${icons[$commit_type]}" + if [[ -n "$icon" ]]; then + echo "$icon" + return 0 + else + return 1 + fi +} + +# extract original message from the first line of file +message=$(head -n 1 <"$commitMessageFile") +echo "$prefix commit subject: '$message'" + +if checkBreakingChangeInBody "$commitMessageFile"; then + echo 'setting breaking change icon' + icon=$boomIcon +else + icon=$(findTypeIcon "$message") + if [ $? -eq 1 ]; then + echo "$prefix โŒ unable to find icon corresponding to commit type. Make sure your commit-lint config (.commitlintrc.js) and append-msg script (append-msg.sh) types match" + exit 1 + fi +fi + +# check if icon has been appended before +if [[ "$message" == *"$icon"* ]]; then + echo "โญ๏ธ skipping icon append as it's been added before" + exit 0 +fi + +# otherwise append icon +updatedMessage="${message/:/: $icon}" + +# replace first line of file with updated message +sed -i "1s/.*/$updatedMessage/" "$commitMessageFile" + +echo "$prefix โœ… appended icon $icon to commit message subject" diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..993e036 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,5 @@ +# run commit lint +npx commitlint --edit "$1" + +# run script to prepend message with icon +./.husky/append-icon.sh "$1" \ No newline at end of file diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg new file mode 100755 index 0000000..d48435c --- /dev/null +++ b/.husky/prepare-commit-msg @@ -0,0 +1,5 @@ +# Check if it's an amend commit +if [ "$2" = "commit" ]; then + echo "Amendment detected, appending icon..." + ./.husky/append-icon.sh "$1" +fi \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..cbcc181 --- /dev/null +++ b/.npmrc @@ -0,0 +1,7 @@ +# Configuration pnpm +auto-install-peers=true +strict-peer-dependencies=false +shamefully-hoist=false + +# Force pnpm usage (prevent npm/yarn) +package-manager=pnpm diff --git a/.prettierrc.json b/.prettierrc.json index d4fb785..a7c797f 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,21 +1,20 @@ { - "trailingComma": "es5", - "tabWidth": 4, - "semi": true, - "singleQuote": true, - "arrowParens": "always", - "bracketSpacing": true, - "endOfLine": "lf", - "htmlWhitespaceSensitivity": "css", - "insertPragma": false, - "singleAttributePerLine": false, - "bracketSameLine": false, - "jsxBracketSameLine": false, - "jsxSingleQuote": true, - "printWidth": 80, - "proseWrap": "preserve", - "quoteProps": "as-needed", - "requirePragma": false, - "useTabs": false, - "embeddedLanguageFormatting": "auto" + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "arrowParens": "always", + "bracketSpacing": true, + "endOfLine": "lf", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "singleAttributePerLine": false, + "bracketSameLine": false, + "jsxSingleQuote": true, + "printWidth": 100, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "useTabs": false, + "embeddedLanguageFormatting": "auto" } diff --git a/Components/Repo/QuickCommands/QuickCommands.js b/Components/Repo/QuickCommands/QuickCommands.js deleted file mode 100644 index c76a3a7..0000000 --- a/Components/Repo/QuickCommands/QuickCommands.js +++ /dev/null @@ -1,70 +0,0 @@ -//Lib -import React from 'react'; -import { useState } from 'react'; -import classes from './QuickCommands.module.css'; -import { IconSettingsAutomation, IconCopy } from '@tabler/icons-react'; - -export default function QuickCommands(props) { - ////Vars - const wizardEnv = props.wizardEnv; - //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled. - let FQDN; - let SSH_SERVER_PORT; - if ( - props.lanCommand && - wizardEnv.FQDN_LAN && - wizardEnv.SSH_SERVER_PORT_LAN - ) { - FQDN = wizardEnv.FQDN_LAN; - SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT_LAN; - } else { - FQDN = wizardEnv.FQDN; - SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT; - } - - //State - const [isCopied, setIsCopied] = useState(false); - - //Functions - const handleCopy = async () => { - // Asynchronously call copy to clipboard - navigator.clipboard - .writeText( - `ssh://${wizardEnv.UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.repositoryName}` - ) - .then(() => { - // If successful, update the isCopied state value - setIsCopied(true); - setTimeout(() => { - setIsCopied(false); - }, 1500); - }) - .catch((err) => { - console.log(err); - }); - }; - - return ( -
- {isCopied ? ( -
Copied !
- ) : ( -
- ssh://{wizardEnv.UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./ - {props.repositoryName} -
- )} - - {props.lanCommand &&
LAN
} - -
- -
- -
-
-
- ); -} diff --git a/Components/Repo/QuickCommands/QuickCommands.module.css b/Components/Repo/QuickCommands/QuickCommands.module.css index 13ce01f..77cc329 100644 --- a/Components/Repo/QuickCommands/QuickCommands.module.css +++ b/Components/Repo/QuickCommands/QuickCommands.module.css @@ -1,116 +1,117 @@ .container { - display: flex; - align-items: center; - align-self: flex-start; - margin: auto 47px auto auto; + display: flex; + align-items: center; + align-self: flex-start; + margin: auto 25px auto auto; } .icons { - position: relative; - bottom: 13px; + position: relative; + bottom: 13px; } .quickSetting { - position: absolute; - visibility: visible; - opacity: 1; + position: absolute; + visibility: visible; + opacity: 1; } .lanBadge { - border-radius: 5px; - border: 1px solid #6d4aff; - color: #6d4aff; - font-size: 0.9em; - padding: 2px 5px; - margin-right: 8px; + border-radius: 5px; + border: 1px solid #6d4aff; + color: #6d4aff; + font-size: 0.9em; + padding: 2px 5px; + margin-right: 8px; } .tooltip { - visibility: hidden; - opacity: 0; - width: 100%; - height: 100%; - border: 1px solid #6d4aff21; - background-color: #f5f5f5; - border-radius: 5px; - box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset; - color: #65748b; - font-size: 0.95rem; - padding: 5px 5px; - transition: 0.5s opacity; + visibility: hidden; + opacity: 0; + width: 100%; + height: 100%; + border: 1px solid #6d4aff21; + background-color: #fafafa; + border-radius: 5px; + box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset; + color: #65748b; + font-size: 0.95rem; + padding: 5px 5px; + transition: 0.5s opacity; } .copyButton { - position: absolute; - visibility: hidden; - opacity: 0; - border: none; - background-color: none; + position: absolute; + visibility: hidden; + opacity: 0; + border: none; + background-color: none; } .copyValid { - margin: auto 8px auto auto; - font-size: 0.95rem; - color: #6d4aff; - animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; + margin: auto 8px auto auto; + padding: 6px 6px; + font-size: 0.95rem; + color: #6d4aff; + animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; } @keyframes scale-in-center { - 0% { - -webkit-transform: scale(0); - transform: scale(0); - opacity: 1; - } - 100% { - -webkit-transform: scale(1); - transform: scale(1); - opacity: 1; - } + 0% { + -webkit-transform: scale(0); + transform: scale(0); + opacity: 1; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 1; + } } /* On Hover */ .container:hover .tooltip { - visibility: visible; - opacity: 1; - width: 100%; - height: 100%; - border: 1px solid #6d4aff21; - background-color: #f5f5f5; - border-radius: 5px; - box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset; - color: #65748b; - font-size: 0.95rem; - padding: 5px 5px; - transition: 0.5s opacity; + visibility: visible; + opacity: 1; + width: 100%; + height: 100%; + border: 1px solid #6d4aff21; + background-color: #fafafa; + border-radius: 5px; + box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset; + color: #65748b; + font-size: 0.95rem; + padding: 5px 5px; + transition: 0.5s opacity; } .container:hover .copyButton { - position: absolute; - visibility: visible; - opacity: 1; - border: none; - background-color: transparent; - cursor: pointer; + position: absolute; + visibility: visible; + opacity: 1; + border: none; + background-color: transparent; + cursor: pointer; } .container:hover .quickSetting { - position: absolute; - visibility: hidden; - opacity: 0; + position: absolute; + visibility: hidden; + opacity: 0; } .container:hover .lanBadge { - visibility: hidden; - opacity: 0; - width: 0; - height: 0; - margin: 0; - padding: 0; + visibility: hidden; + opacity: 0; + width: 0; + height: 0; + margin: 0; + padding: 0; } @media all and (max-width: 1000px) { - .container { - display: none; - } + .container { + display: none; + } } diff --git a/Components/Repo/QuickCommands/QuickCommands.tsx b/Components/Repo/QuickCommands/QuickCommands.tsx new file mode 100644 index 0000000..8fe57b3 --- /dev/null +++ b/Components/Repo/QuickCommands/QuickCommands.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { useState } from 'react'; +import classes from './QuickCommands.module.css'; +import { IconSettingsAutomation, IconCopy } from '@tabler/icons-react'; +import { lanCommandOption } from '~/helpers/functions'; +import { WizardEnvType } from '~/types/domain/config.types'; + +type QuickCommandsProps = { + repositoryName: string; + wizardEnv?: WizardEnvType; + lanCommand?: boolean; +}; + +export default function QuickCommands(props: QuickCommandsProps) { + const wizardEnv = props.wizardEnv; + //Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled. + const { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.lanCommand); + + const [isCopied, setIsCopied] = useState(false); + + const handleCopy = async () => { + // Asynchronously call copy to clipboard + navigator.clipboard + .writeText( + `ssh://${wizardEnv?.UNIX_USER}@${FQDN}${SSH_SERVER_PORT ? SSH_SERVER_PORT : ''}/./${props.repositoryName}` + ) + .then(() => { + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 1500); + }) + .catch((err) => { + console.log(err); + }); + }; + + return ( +
+ {isCopied ? ( +
Copied !
+ ) : ( +
+ ssh://{wizardEnv?.UNIX_USER}@{FQDN} + {SSH_SERVER_PORT ? SSH_SERVER_PORT : ''}/./ + {props.repositoryName} +
+ )} + + {props.lanCommand &&
LAN
} + +
+ +
+ +
+
+
+ ); +} diff --git a/Components/Repo/Repo.js b/Components/Repo/Repo.js deleted file mode 100644 index 2dce1ae..0000000 --- a/Components/Repo/Repo.js +++ /dev/null @@ -1,217 +0,0 @@ -//Lib -import { useState } from 'react'; -import classes from './Repo.module.css'; -import { - IconSettings, - IconInfoCircle, - IconChevronDown, - IconChevronUp, - IconBellOff, - IconLockPlus, -} from '@tabler/icons-react'; -import timestampConverter from '../../helpers/functions/timestampConverter'; -import StorageBar from '../UI/StorageBar/StorageBar'; -import QuickCommands from './QuickCommands/QuickCommands'; - -export default function Repo(props) { - //Load displayDetails from LocalStorage - const displayDetailsFromLS = () => { - try { - if ( - localStorage.getItem('displayDetailsRepo' + props.id) === null - ) { - localStorage.setItem( - 'displayDetailsRepo' + props.id, - JSON.stringify(true) - ); - return true; - } else { - return JSON.parse( - localStorage.getItem('displayDetailsRepo' + props.id) - ); - } - } catch (error) { - console.log( - 'LocalStorage error, key', - 'displayDetailsRepo' + props.id, - 'will be removed. Try again.', - 'Error message on this key : ', - error - ); - localStorage.removeItem('displayDetailsRepo' + props.id); - } - }; - - //States - const [displayDetails, setDisplayDetails] = useState(displayDetailsFromLS); - - //BUTTON : Display or not repo details for ONE repo - const displayDetailsForOneHandler = (boolean) => { - //Update localStorage - localStorage.setItem( - 'displayDetailsRepo' + props.id, - JSON.stringify(boolean) - ); - setDisplayDetails(boolean); - }; - - //Status indicator - const statusIndicator = () => { - return props.status - ? classes.statusIndicatorGreen - : classes.statusIndicatorRed; - }; - - //Alert indicator - const alertIndicator = () => { - if (props.alert === 0) { - return ( -
- -
- ); - } - }; - - const appendOnlyModeIndicator = () => { - if (props.appendOnlyMode) { - return ( -
- -
- ); - } - }; - - return ( - <> - {displayDetails ? ( - <> -
-
-
-
{props.alias}
- {appendOnlyModeIndicator()} - {alertIndicator()} - {props.comment && ( -
- -
- {props.comment} -
-
- )} - -
- - - - - - - - - - - - - - - - - - - - - - -
Repository - Storage Size - - Storage Used - - Last change - IDEdit
{props.repositoryName}{props.storageSize} GB - - -
- {props.lastSave === 0 - ? '-' - : timestampConverter( - props.lastSave - )} -
-
#{props.id} -
- - props.repoManageEditHandler() - } - /> -
-
-
- - ) : ( - <> -
-
-
-
{props.alias}
- {appendOnlyModeIndicator()} - {alertIndicator()} - {props.comment && ( -
- -
- {props.comment} -
-
- )} -
-
- {props.lastSave === 0 - ? null - : timestampConverter(props.lastSave)} - - #{props.id} - -
-
- - )} - {displayDetails ? ( -
- { - displayDetailsForOneHandler(false); - }} - /> -
- ) : ( -
- { - displayDetailsForOneHandler(true); - }} - /> -
- )} - - ); -} diff --git a/Components/Repo/Repo.module.css b/Components/Repo/Repo.module.css index edbf05e..8311137 100644 --- a/Components/Repo/Repo.module.css +++ b/Components/Repo/Repo.module.css @@ -1,250 +1,309 @@ /*Repo CLOSE*/ .RepoClose { - display: flex; - justify-content: space-between; - align-items: center; - box-shadow: - 0 1px 3px rgba(0, 0, 0, 0.12), - 0 1px 2px rgba(0, 0, 0, 0.24); - width: auto; - max-height: 65px; - margin: 20px 0px 0px 0px; - border-radius: 5px; - overflow: visible; - /* Need to display comment on hover (which is position : absolute) */ - position: relative; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.24); + width: auto; + max-height: 65px; + margin: 20px 0px 0px 0px; + border-radius: 5px; + overflow: visible; + /* Need to display comment on hover (which is position : absolute) */ + position: relative; + background: #fff; } .closeFlex { - display: flex; - align-items: center; - padding: 15px; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 15px; + gap: 10px; } .RepoClose .lastSave { - padding: 15px; + white-space: nowrap; +} + +.RepoClose .leftGroup { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + gap: 10px; +} + +.RepoClose .alias { + font-weight: bold; + color: #111827; + font-size: 1.05em; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex: 1; + min-width: 0; } /* REPO OPEN */ .RepoOpen { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - box-shadow: - 0 1px 3px rgba(0, 0, 0, 0.12), - 0 1px 2px rgba(0, 0, 0, 0.24); - width: auto; - max-height: 200px; - margin: 20px 0px 0px 0px; - padding: 15px; - border-radius: 5px; - transition: max-height 0.1s linear; - overflow: visible; - /* Need to display comment on hover (which is position : absolute) */ - position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.24); + width: auto; + margin: 20px 0px 0px 0px; + padding: 15px; + border-radius: 5px; + transition: max-height 0.1s linear; + overflow: visible; + /* Need to display comment on hover (which is position : absolute) */ + position: relative; + background: #fff; } .openFlex { - display: flex; - align-items: center; - align-self: flex-start; - width: 100%; + display: block; + width: 100%; +} + +.aliasFlex { + display: flex; + flex-direction: row; + align-items: center; + width: 100%; +} +.indicatorsFlex { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + width: 100%; + gap: 15px; } .tabInfo { - width: 100%; - overflow-wrap: break-word; - border-collapse: collapse; - background: #fff; - border-radius: 10px; - overflow: hidden; - margin: 25px auto; - table-layout: fixed; + width: 100%; + overflow-wrap: break-word; + border-collapse: collapse; + background: #fff; + border-radius: 10px; + overflow: hidden; + margin: 15px auto; + table-layout: fixed; } .tabInfo thead tr { - height: 50px; - background: #111827; - color: #fff; + height: 50px; + background: #111827; + color: #fff; } .tabInfo thead th { - font-size: 1em; - color: #fff; - line-height: 1.2; - font-weight: normal; + font-size: 1em; + color: #fff; + line-height: 1.2; + font-weight: 500; } .tabInfo tbody tr { - background-color: #f3f4f6; - height: 50px; + background-color: #f3f4f6; + height: 50px; } .tabInfo tbody tr th { - color: #65748b; - font-size: 0.95rem; - font-weight: 400; + color: #65748b; + font-size: 0.95rem; + font-weight: 400; } /*STATUS*/ - -.statusIndicatorGreen { - background: rgb(9, 255, 0); - border-radius: 50%; - margin: 10px; - height: 15px; - width: 15px; - box-shadow: 0 0 0 0 rgb(9, 255, 0); - transform: scale(1); - animation: pulseGreen 5s infinite; - animation-delay: 1s; +.statusIndicatorGreen, +.statusIndicatorRed { + border-radius: 50%; + height: 16px; + width: 16px; + flex-shrink: 0; + animation: pulse 5s infinite; } -@keyframes pulseGreen { - 0% { - transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(17, 255, 0, 0.7); - } - - 10% { - transform: scale(1); - box-shadow: 0 0 0 10px rgba(17, 255, 0, 0); - } - - 90% { - transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(17, 255, 0, 0); - } +.statusIndicatorGreen { + background: #00d26a; + box-shadow: 0 0 0 0 rgba(0, 210, 106, 0.7); } .statusIndicatorRed { - background: rgb(255, 0, 0); - border-radius: 50%; - margin: 10px; - height: 15px; - width: 15px; - - box-shadow: 0 0 0 0 rgb(255, 0, 0); - transform: scale(1); - animation: pulseRed 5s infinite; - animation-delay: 0.5s; + background: #ff3d3d; + box-shadow: 0 0 0 0 rgba(255, 61, 61, 0.7); + animation-delay: 0.5s; } -@keyframes pulseRed { - 0% { - transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7); - } - - 10% { - transform: scale(1); - box-shadow: 0 0 0 10px rgba(255, 0, 0, 0); - } - - 90% { - transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); - } +@keyframes pulse { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.4); + } + 10% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); + } + 90% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); + } } /* Alert icon */ - .alertIcon { - display: flex; - flex-direction: row; - align-items: center; - margin-left: 10px; + display: flex; + flex-direction: row; + align-items: center; } .appendOnlyModeIcon { - display: flex; - flex-direction: row; - align-items: center; - margin-left: 10px; + display: flex; + flex-direction: row; + align-items: center; } /* GENERAL */ .alias { - font-weight: bold; - color: #111827; - font-size: 1.05em; + font-weight: bold; + color: #111827; + font-size: 1.05em; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.RepoOpen .alias { + margin-top: 5px; } .lastSave { - color: #65748b; + color: #65748b; } .editButton { - cursor: pointer; + cursor: pointer; } /* Comment */ .comment { - display: flex; - flex-direction: row; - align-items: center; - margin-left: 10px; + display: flex; + flex-direction: row; + align-items: center; } .toolTip { - visibility: hidden; - width: auto; - height: auto; - max-width: 400px; - max-height: 250px; - background-color: #fff; - color: #637381; - text-align: center; - border-radius: 6px; - padding: 5px 5px; - position: absolute; - z-index: 1; - margin: 0px 0 0 20px; - opacity: 1; - transition: 0.5s opacity; - box-shadow: - 0 3px 6px rgba(0, 0, 0, 0.16), - 0 3px 6px rgba(0, 0, 0, 0.23); - overflow: auto; + visibility: hidden; + width: auto; + height: auto; + max-width: 400px; + max-height: 250px; + background-color: #fff; + color: #637381; + text-align: center; + border-radius: 6px; + padding: 5px 5px; + position: absolute; + z-index: 1; + margin: 0px 0 0 20px; + opacity: 1; + transition: 0.5s opacity; + box-shadow: + 0 3px 6px rgba(0, 0, 0, 0.16), + 0 3px 6px rgba(0, 0, 0, 0.23); + overflow: auto; } .comment:hover .toolTip, .comment:active .toolTip { - visibility: visible; - opacity: 1; + visibility: visible; + opacity: 1; } .chevron { - margin: auto; + margin: auto; } .chevron :focus, .chevron :hover { - cursor: pointer; - filter: invert(27%) sepia(82%) saturate(2209%) hue-rotate(240deg) - brightness(99%) contrast(105%); + cursor: pointer; + filter: invert(27%) sepia(82%) saturate(2209%) hue-rotate(240deg) brightness(99%) contrast(105%); } /* MOBILE */ @media all and (max-width: 1000px) { - .tabInfo { - display: none; - } - .toolTip { - display: none; - } - .comment { - display: none; - } - .lastSave { - display: none; - } - .closeFlex { - margin: auto; - } - .openFlex { - margin: auto; - width: auto; - } + .openFlex, + .tabInfo, + .toolTip, + .comment, + .chevron { + display: none !important; + } + + .RepoOpen, + .RepoClose { + display: flex !important; + flex-direction: row !important; + align-items: center !important; + justify-content: space-between !important; + max-height: 65px !important; + padding: 15px !important; + margin: 20px 0 0 0 !important; + } + + .closeFlex { + display: flex !important; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0 !important; + margin: 0 !important; + } + + .alias { + flex: 1; + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .leftGroup { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + gap: 10px; + } + + .rightGroup { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; + } + + .lastSave { + display: block !important; + color: #65748b; + white-space: nowrap; + font-size: 0.85em; + flex-shrink: 0; + margin-left: 10px; + } + + .appendOnlyModeIcon, + .alertIcon { + display: flex; + align-items: center; + } } diff --git a/Components/Repo/Repo.tsx b/Components/Repo/Repo.tsx new file mode 100644 index 0000000..a659670 --- /dev/null +++ b/Components/Repo/Repo.tsx @@ -0,0 +1,261 @@ +import { useState, useMemo } from 'react'; +import classes from './Repo.module.css'; +import { + IconSettings, + IconInfoCircle, + IconChevronDown, + IconChevronUp, + IconBellOff, + IconLockPlus, +} from '@tabler/icons-react'; +import StorageBar from '../UI/StorageBar/StorageBar'; +import QuickCommands from './QuickCommands/QuickCommands'; +import { Repository, WizardEnvType, Optional } from '~/types'; +import { fromUnixTime, formatDistanceStrict } from 'date-fns'; +import useMedia from 'use-media'; + +type RepoProps = Omit & { + repoManageEditHandler: () => void; + wizardEnv: Optional; +}; + +export default function Repo(props: RepoProps) { + const isMobile = useMedia({ maxWidth: 1000 }); + + const currentDate = useMemo(() => new Date(), []); + + //Load displayDetails from LocalStorage + const displayDetailsFromLS = (): boolean => { + const key = `displayDetailsRepo${props.id}`; + + try { + const storedValue = localStorage.getItem(key); + + if (storedValue === null) { + const defaultValue = true; + localStorage.setItem(key, JSON.stringify(defaultValue)); + return defaultValue; + } + + const parsedValue = JSON.parse(storedValue); + if (typeof parsedValue === 'boolean') { + return parsedValue; + } + + localStorage.removeItem(key); + return true; + } catch (error) { + localStorage.removeItem(key); + return true; + } + }; + + //States + const [displayDetails, setDisplayDetails] = useState(displayDetailsFromLS); + + //BUTTON : Display or not repo details for ONE repo + const displayDetailsForOneHandler = (boolean: boolean) => { + //Update localStorage + localStorage.setItem('displayDetailsRepo' + props.id, JSON.stringify(boolean)); + setDisplayDetails(boolean); + }; + + //Status indicator + const statusIndicator = () => { + return props.status ? classes.statusIndicatorGreen : classes.statusIndicatorRed; + }; + + //Alert indicator + const alertIndicator = () => { + if (props.alert === 0) { + return ( +
+ +
+ ); + } + }; + + const appendOnlyModeIndicator = () => { + if (props.appendOnlyMode) { + return ( +
+ +
+ ); + } + }; + + const mobileView = () => { + return ( + <> +
+
+
+
+
{props.alias}
+
+ {appendOnlyModeIndicator()} + {alertIndicator()} + {props.comment && ( +
+ +
{props.comment}
+
+ )} + +
+ + {props.lastSave === 0 + ? '-' + : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, { + addSuffix: true, + })} + +
+
+
+ + ); + }; + + if (isMobile) { + return mobileView(); + } else { + return ( + <> + {displayDetails ? ( + <> +
+
+
+ {props.comment && ( +
+ +
{props.comment}
+
+ )} + {appendOnlyModeIndicator()} + {alertIndicator()} + +
+
+
{props.alias}
+
+ + + + + + + + + + + + + + + + + + + + +
RepositoryStorage SizeStorage UsedLast changeEdit
{props.repositoryName}{props.storageSize} GB + + +
+ {props.lastSave === 0 + ? '-' + : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, { + addSuffix: true, + })} +
+
+
+ props.repoManageEditHandler()} + /> +
+
+
+ + ) : ( + <> +
+
+
+
+
{props.alias}
+
+ {appendOnlyModeIndicator()} + {alertIndicator()} + {props.comment && ( +
+ +
{props.comment}
+
+ )} + +
+ + {props.lastSave === 0 + ? '-' + : formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, { + addSuffix: true, + })} + +
+
+
+ + )} + {displayDetails ? ( +
+ { + displayDetailsForOneHandler(false); + }} + /> +
+ ) : ( +
+ { + displayDetailsForOneHandler(true); + }} + /> +
+ )} + + ); + } +} diff --git a/Components/UI/CopyButton/CopyButton.js b/Components/UI/CopyButton/CopyButton.js deleted file mode 100644 index 0079d35..0000000 --- a/Components/UI/CopyButton/CopyButton.js +++ /dev/null @@ -1,39 +0,0 @@ -//Lib -import classes from './CopyButton.module.css'; -import { useState } from 'react'; -import { IconCopy } from '@tabler/icons-react'; - -export default function CopyButton(props) { - //State - const [isCopied, setIsCopied] = useState(false); - - //Function - const handleCopy = async (data) => { - navigator.clipboard - .writeText(data) - .then(() => { - // If successful, update the isCopied state value - setIsCopied(true); - setTimeout(() => { - setIsCopied(false); - }, 1500); - }) - .catch((err) => { - console.log(err); - }); - }; - - return ( - <> - - {isCopied ? ( - Copied ! - ) : null} - - ); -} diff --git a/Components/UI/CopyButton/CopyButton.module.css b/Components/UI/CopyButton/CopyButton.module.css index ab59788..9e8a4ea 100644 --- a/Components/UI/CopyButton/CopyButton.module.css +++ b/Components/UI/CopyButton/CopyButton.module.css @@ -1,26 +1,35 @@ .copyButton { - visibility: visible; - opacity: 1; - border: none; - background-color: transparent; - cursor: pointer; + visibility: visible; + opacity: 1; + border: none; + background-color: transparent; + cursor: pointer; + display: flex; + align-items: center; +} + +.copyButton span { + font-size: 0.95rem; + color: #6d4aff; + margin-right: 5px; + user-select: text; } .copyValid { - font-size: 0.95rem; - color: #6d4aff; - animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; + font-size: 0.95rem; + color: #6d4aff; + animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; } @keyframes scale-in-center { - 0% { - -webkit-transform: scale(0); - transform: scale(0); - opacity: 1; - } - 100% { - -webkit-transform: scale(1); - transform: scale(1); - opacity: 1; - } + 0% { + -webkit-transform: scale(0); + transform: scale(0); + opacity: 1; + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 1; + } } diff --git a/Components/UI/CopyButton/CopyButton.tsx b/Components/UI/CopyButton/CopyButton.tsx new file mode 100644 index 0000000..1258f39 --- /dev/null +++ b/Components/UI/CopyButton/CopyButton.tsx @@ -0,0 +1,46 @@ +import classes from './CopyButton.module.css'; +import { useState, ReactNode } from 'react'; +import { IconChecks, IconCopy } from '@tabler/icons-react'; + +type CopyButtonProps = { + dataToCopy: string; + children?: ReactNode; + displayIconConfirmation?: boolean; + size?: number; + stroke?: number; +}; + +export default function CopyButton(props: CopyButtonProps) { + const [isCopied, setIsCopied] = useState(false); + + const handleCopy = async (data: string) => { + navigator.clipboard + .writeText(data) + .then(() => { + // If successful, update the isCopied state value + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 1500); + }) + .catch((err) => { + console.log(err); + }); + }; + + return ( + <> + + {isCopied + ? !props.displayIconConfirmation && Copied ! + : null} + + ); +} diff --git a/Components/UI/Error/Error.js b/Components/UI/Error/Error.js deleted file mode 100644 index 2a3fe1f..0000000 --- a/Components/UI/Error/Error.js +++ /dev/null @@ -1,6 +0,0 @@ -//Lib -import classes from './Error.module.css'; - -export default function Error(props) { - return
{props.message}
; -} diff --git a/Components/UI/Error/Error.module.css b/Components/UI/Error/Error.module.css index 19b75f6..9947cb2 100644 --- a/Components/UI/Error/Error.module.css +++ b/Components/UI/Error/Error.module.css @@ -1,18 +1,18 @@ .errorMessage { - margin: 15px 0px; - background-color: red; - color: white; - padding: 15px; - border-radius: 5px; - animation: myAnim 1s ease 0s 1 normal forwards; + margin: 15px 0px; + background-color: red; + color: white; + padding: 15px; + border-radius: 5px; + animation: myAnim 1s ease 0s 1 normal forwards; } @keyframes myAnim { - 0% { - opacity: 0; - } + 0% { + opacity: 0; + } - 100% { - opacity: 1; - } + 100% { + opacity: 1; + } } diff --git a/Components/UI/Error/Error.tsx b/Components/UI/Error/Error.tsx new file mode 100644 index 0000000..d0c4da8 --- /dev/null +++ b/Components/UI/Error/Error.tsx @@ -0,0 +1,9 @@ +import classes from './Error.module.css'; + +type ErrorProps = { + message: string; +}; + +export default function Error(props: ErrorProps) { + return
{props.message}
; +} diff --git a/Components/UI/Info/Info.js b/Components/UI/Info/Info.js deleted file mode 100644 index e06c0bd..0000000 --- a/Components/UI/Info/Info.js +++ /dev/null @@ -1,6 +0,0 @@ -//Lib -import classes from './Info.module.css'; - -export default function Info(props) { - return
{props.message}
; -} diff --git a/Components/UI/Info/Info.module.css b/Components/UI/Info/Info.module.css index 0bb624d..d6df06d 100644 --- a/Components/UI/Info/Info.module.css +++ b/Components/UI/Info/Info.module.css @@ -1,18 +1,18 @@ .infoMessage { - margin: 15px 0px; - background-color: rgb(17, 147, 0); - color: white; - padding: 15px; - border-radius: 5px; - animation: myAnim 1s ease 0s 1 normal forwards; + margin: 15px 0px; + background-color: rgb(17, 147, 0); + color: white; + padding: 15px; + border-radius: 5px; + animation: myAnim 1s ease 0s 1 normal forwards; } @keyframes myAnim { - 0% { - opacity: 0; - } + 0% { + opacity: 0; + } - 100% { - opacity: 1; - } + 100% { + opacity: 1; + } } diff --git a/Components/UI/Info/Info.tsx b/Components/UI/Info/Info.tsx new file mode 100644 index 0000000..8e2f729 --- /dev/null +++ b/Components/UI/Info/Info.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; +import classes from './Info.module.css'; + +type InfoProps = { + message: string; + color?: string; + children?: ReactNode; +}; + +export default function Info(props: InfoProps) { + return ( +
+ {props.message} + {props.children} +
+ ); +} diff --git a/Components/UI/Layout/Footer/Footer.js b/Components/UI/Layout/Footer/Footer.js deleted file mode 100644 index 10e2566..0000000 --- a/Components/UI/Layout/Footer/Footer.js +++ /dev/null @@ -1,24 +0,0 @@ -//Lib -import classes from './Footer.module.css'; -import packageInfo from '../../../../package.json'; - -function Footer() { - return ( -
-

- About{' '} - - BorgWarehouse - {' '} - - v{packageInfo.version} -

-
- ); -} - -export default Footer; diff --git a/Components/UI/Layout/Footer/Footer.module.css b/Components/UI/Layout/Footer/Footer.module.css index c966167..e19fc52 100644 --- a/Components/UI/Layout/Footer/Footer.module.css +++ b/Components/UI/Layout/Footer/Footer.module.css @@ -1,26 +1,25 @@ .footer { - color: #494b7a; - text-align: center; - position: absolute; - bottom: 0; - width: 100%; - height: 50px; + color: #494b7a; + text-align: center; + position: absolute; + bottom: 0; + width: 100%; } .footer p { - padding-left: 70px; + padding-left: 70px; } a.site { - color: #6d4aff; - text-decoration: none; + color: #6d4aff; + text-decoration: none; } @media all and (max-width: 1000px) { - .footer { - width: 100%; - } - .footer p { - padding-left: 0; - } + .footer { + width: 100%; + } + .footer p { + padding-left: 0; + } } diff --git a/Components/UI/Layout/Footer/Footer.tsx b/Components/UI/Layout/Footer/Footer.tsx new file mode 100644 index 0000000..58cd034 --- /dev/null +++ b/Components/UI/Layout/Footer/Footer.tsx @@ -0,0 +1,23 @@ +import classes from './Footer.module.css'; +import packageInfo from '~/package.json'; + +function Footer() { + return ( +
+

+ About{' '} + + BorgWarehouse + {' '} + - v{packageInfo.version} +

+
+ ); +} + +export default Footer; diff --git a/Components/UI/Layout/Header/Header.js b/Components/UI/Layout/Header/Header.js deleted file mode 100644 index 9bb2f64..0000000 --- a/Components/UI/Layout/Header/Header.js +++ /dev/null @@ -1,21 +0,0 @@ -//Lib -import classes from './Header.module.css'; - -//Components -import Nav from './Nav/Nav'; - -function Header() { - return ( -
-
-
BorgWarehouse
- -
-
- ); -} - -export default Header; diff --git a/Components/UI/Layout/Header/Header.module.css b/Components/UI/Layout/Header/Header.module.css index c213172..2156d01 100644 --- a/Components/UI/Layout/Header/Header.module.css +++ b/Components/UI/Layout/Header/Header.module.css @@ -1,31 +1,31 @@ .Header { - width: 100%; - background: #111827; - box-shadow: - 0 3px 6px rgba(0, 0, 0, 0.16), - 0 3px 6px rgba(0, 0, 0, 0.23); - height: 50px; - color: white; - display: flex; - align-items: center; - position: static; - top: 0; - z-index: 1000; + width: 100%; + background: #111827; + box-shadow: + 0 3px 6px rgba(0, 0, 0, 0.16), + 0 3px 6px rgba(0, 0, 0, 0.23); + height: 50px; + color: white; + display: flex; + align-items: center; + position: static; + top: 0; + z-index: 1000; } .flex { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - max-width: 1500px; - margin: auto; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + max-width: 1500px; + margin: auto; } .logo { - font-size: 1.5em; - font-weight: bold; - color: #6d4aff; - text-shadow: #6d4aff 0px 0px 18px; - margin-left: 20px; + font-size: 1.5em; + font-weight: bold; + color: #6d4aff; + text-shadow: #6d4aff 0px 0px 18px; + margin-left: 70px; } diff --git a/Components/UI/Layout/Header/Header.tsx b/Components/UI/Layout/Header/Header.tsx new file mode 100644 index 0000000..1067f6e --- /dev/null +++ b/Components/UI/Layout/Header/Header.tsx @@ -0,0 +1,28 @@ +import Image from 'next/image'; +import classes from './Header.module.css'; +import Nav from './Nav/Nav'; + +function Header() { + return ( +
+
+
+ BorgWarehouse +
+ +
+
+ ); +} + +export default Header; diff --git a/Components/UI/Layout/Header/Nav/Nav.js b/Components/UI/Layout/Header/Nav/Nav.js deleted file mode 100644 index 24d035b..0000000 --- a/Components/UI/Layout/Header/Nav/Nav.js +++ /dev/null @@ -1,57 +0,0 @@ -//Lib -import classes from './Nav.module.css'; -import { IconUser, IconLogout } from '@tabler/icons-react'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; -import { useSession, signOut } from 'next-auth/react'; - -export default function Nav() { - ////Var - //Get the current route to light the right Item - const router = useRouter(); - const currentRoute = router.pathname; - const { status, data } = useSession(); - - //Function - const onLogoutClickedHandler = async () => { - //This bug is open : https://github.com/nextauthjs/next-auth/issues/1542 - //I put redirect to false and redirect with router. - //The result on logout click is an ugly piece of page for a few milliseconds before returning to the login page. - //It's ugly if you are perfectionist but functional and invisible for most of users while waiting for a next-auth fix. - await signOut({ redirect: false }); - router.replace('/login'); - }; - - return ( -
    -
  • - -
    -
    - -
    -
    - {status === 'authenticated' && data.user.name} -
    -
    - -
  • - -
  • -
    - - - -
    -
  • -
- ); -} diff --git a/Components/UI/Layout/Header/Nav/Nav.module.css b/Components/UI/Layout/Header/Nav/Nav.module.css index 5b8c396..8032f69 100644 --- a/Components/UI/Layout/Header/Nav/Nav.module.css +++ b/Components/UI/Layout/Header/Nav/Nav.module.css @@ -1,54 +1,54 @@ .Nav { - list-style-type: none; - margin: 0px 15px 0px 0px; - padding: 0; - display: flex; + list-style-type: none; + margin: 0px 15px 0px 0px; + padding: 0; + display: flex; } .user { - display: flex; - align-items: center; + display: flex; + align-items: center; } .username::first-letter { - text-transform: capitalize; + text-transform: capitalize; } .account { - background: none; - border: none; - cursor: pointer; - color: #494b7a; + background: none; + border: none; + cursor: pointer; + color: #494b7a; } .account a { - color: #494b7a; - text-decoration: none; + color: #494b7a; + text-decoration: none; } .account :focus, .account .active, .account :hover { - color: #6d4aff; - text-shadow: #6d4aff 0px 0px 18px; + color: #6d4aff; + text-shadow: #6d4aff 0px 0px 18px; } .logout { - background: none; - border: none; - cursor: pointer; - color: #494b7a; + background: none; + border: none; + cursor: pointer; + color: #494b7a; } .logout :focus, .logout .active, .logout :hover { - color: #6d4aff; - text-shadow: #6d4aff 0px 0px 18px; + color: #6d4aff; + text-shadow: #6d4aff 0px 0px 18px; } @media all and (max-width: 1000px) { - .account { - display: none; - } + .account { + display: none; + } } diff --git a/Components/UI/Layout/Header/Nav/Nav.tsx b/Components/UI/Layout/Header/Nav/Nav.tsx new file mode 100644 index 0000000..1ccba41 --- /dev/null +++ b/Components/UI/Layout/Header/Nav/Nav.tsx @@ -0,0 +1,43 @@ +import classes from './Nav.module.css'; +import { IconUser, IconLogout } from '@tabler/icons-react'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { useSession, signOut } from 'next-auth/react'; + +export default function Nav() { + const router = useRouter(); + const currentRoute = router.pathname; + const { status, data } = useSession(); + + const onLogoutClickedHandler = async () => { + //This bug is open : https://github.com/nextauthjs/next-auth/issues/1542 + //I put redirect to false and redirect with router. + //The result on logout click is an ugly piece of page for a few milliseconds before returning to the login page. + //It's ugly if you are perfectionist but functional and invisible for most of users while waiting for a next-auth fix. + await signOut({ redirect: false }); + router.replace('/login'); + }; + + return ( +
    +
  • + +
    +
    + +
    +
    {status === 'authenticated' && data.user?.name}
    +
    + +
  • + +
  • +
    + + + +
    +
  • +
+ ); +} diff --git a/Components/UI/Layout/Layout.js b/Components/UI/Layout/Layout.js deleted file mode 100644 index 96fcedd..0000000 --- a/Components/UI/Layout/Layout.js +++ /dev/null @@ -1,30 +0,0 @@ -//Lib -import Footer from './Footer/Footer'; -import Header from './Header/Header'; -import NavSide from './NavSide/NavSide'; -import classes from './Layout.module.css'; -import { useSession } from 'next-auth/react'; - -function Layout(props) { - //Var - const { status } = useSession(); - - if (status === 'authenticated') { - return ( - <> -
- -
{props.children}
-