diff --git a/.eslintrc.json b/.eslintrc.json index 168665fc..215d6491 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,7 +4,7 @@ "es2020": true, "node": true }, - "extends": ["eslint:recommended", "plugin:react/recommended", "prettier", "next"], + "extends": ["eslint:recommended", "plugin:prettier/recommended", "next"], "parserOptions": { "ecmaFeatures": { "jsx": true @@ -12,11 +12,12 @@ "ecmaVersion": 11, "sourceType": "module" }, - "plugins": ["react"], "rules": { "react/display-name": "off", "react/react-in-jsx-scope": "off", - "react/prop-types": "off" + "react/prop-types": "off", + "import/no-anonymous-default-export": "off", + "@next/next/no-img-element": "off" }, "globals": { "React": "writable" diff --git a/.github/workflows/cd-manual.yml b/.github/workflows/cd-manual.yml new file mode 100644 index 00000000..6b04dd84 --- /dev/null +++ b/.github/workflows/cd-manual.yml @@ -0,0 +1,31 @@ +name: Create docker images (manual) + +on: + workflow_dispatch: + inputs: + version: + type: string + description: Version + required: true + +jobs: + build: + name: Build, push, and deploy + runs-on: ubuntu-latest + + strategy: + matrix: + db-type: [postgresql, mysql] + + steps: + - uses: actions/checkout@v2 + + - uses: mr-smithers-excellent/docker-build-push@v5 + name: Build & push Docker image for ${{ matrix.db-type }} + with: + image: umami + tags: ${{ matrix.db-type }}-${{ inputs.version }}, ${{ matrix.db-type }}-latest + buildArgs: DATABASE_TYPE=${{ matrix.db-type }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..6d8a7f73 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,30 @@ +name: Create docker images + +on: [create] + +jobs: + + build: + name: Build, push, and deploy + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + runs-on: ubuntu-latest + + strategy: + matrix: + db-type: [postgresql, mysql] + + steps: + - uses: actions/checkout@v2 + + - name: Set env + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - uses: mr-smithers-excellent/docker-build-push@v5 + name: Build & push Docker image for ${{ matrix.db-type }} + with: + image: umami + tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest + buildArgs: DATABASE_TYPE=${{ matrix.db-type }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..80db33c6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: [push] + +env: + DATABASE_TYPE: postgresql + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + include: + - node-version: 14.x + db-type: postgresql + - node-version: 14.x + db-type: mysql + - node-version: 16.x + db-type: postgresql + - node-version: 16.x + db-type: mysql + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + env: + DATABASE_TYPE: ${{ matrix.db-type }} + - run: npm install --global yarn + - run: yarn install --frozen-lockfile + - run: yarn build diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index ec7dfa4c..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,42 +0,0 @@ -on: - push: - branches: - - master - -jobs: - - build: - name: Build, push, and deploy - runs-on: ubuntu-latest - steps: - - - name: Checkout master - uses: actions/checkout@v2 - - - name: Build PostgreSQL container image - run: | - docker build --build-arg DATABASE_TYPE=postgresql \ - --tag ghcr.io/$GITHUB_ACTOR/umami:postgresql-$(echo $GITHUB_SHA | head -c7) \ - --tag ghcr.io/$GITHUB_ACTOR/umami:postgresql-latest \ - . - - - name: Build MySQL container image - run: | - docker build --build-arg DATABASE_TYPE=mysql \ - --tag ghcr.io/$GITHUB_ACTOR/umami:mysql-$(echo $GITHUB_SHA | head -c7) \ - --tag ghcr.io/$GITHUB_ACTOR/umami:mysql-latest \ - . - - - name: Docker login - run: >- - echo "${{ secrets.GITHUB_TOKEN }}" - | docker login -u "${{ github.actor }}" --password-stdin ghcr.io - - - name: Push image to GitHub - run: | - # Push each image individually, avoiding pushing to umami:latest - # as MySQL or PostgreSQL are required - docker push ghcr.io/$GITHUB_ACTOR/umami:postgresql-$(echo $GITHUB_SHA | head -c7) - docker push ghcr.io/$GITHUB_ACTOR/umami:postgresql-latest - docker push ghcr.io/$GITHUB_ACTOR/umami:mysql-$(echo $GITHUB_SHA | head -c7) - docker push ghcr.io/$GITHUB_ACTOR/umami:mysql-latest diff --git a/.gitignore b/.gitignore index e6b35441..6414cc5f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ # next.js /.next/ /out/ -/prisma/schema.prisma +/prisma/ # production /build diff --git a/.stylelintrc.json b/.stylelintrc.json index 117fac2a..9a05af14 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -13,5 +13,5 @@ } ] }, - "ignoreFiles": ["**/*.js"] + "ignoreFiles": ["**/*.js", "**/*.md"] } diff --git a/Dockerfile b/Dockerfile index e66ec5d5..48f3df32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,43 +1,55 @@ -# Build image -FROM node:12.22-alpine AS build -ARG BASE_PATH -ARG DATABASE_TYPE -ENV BASE_PATH=$BASE_PATH -ENV DATABASE_URL "postgresql://umami:umami@db:5432/umami" \ - DATABASE_TYPE=$DATABASE_TYPE -WORKDIR /build - -RUN yarn config set --home enableTelemetry 0 -COPY package.json yarn.lock /build/ - -# Install only the production dependencies -RUN yarn install --production --frozen-lockfile - -# Cache these modules for production -RUN cp -R node_modules/ prod_node_modules/ - -# Install development dependencies +# Install dependencies only when needed +FROM node:16-alpine AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app +COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile -COPY . /build -RUN yarn next telemetry disable +# Rebuild the source code only when needed +FROM node:16-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ARG DATABASE_TYPE +ARG BASE_PATH + +ENV DATABASE_TYPE $DATABASE_TYPE +ENV BASE_PATH $BASE_PATH + +ENV NEXT_TELEMETRY_DISABLED 1 + RUN yarn build -# Production image -FROM node:12.22-alpine AS production +# Production image, copy all the files and run next +FROM node:16-alpine AS runner WORKDIR /app -# Copy cached dependencies -COPY --from=build /build/prod_node_modules ./node_modules +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 -# Copy generated Prisma client -COPY --from=build /build/node_modules/.prisma/ ./node_modules/.prisma/ +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs -COPY --from=build /build/yarn.lock /build/package.json ./ -COPY --from=build /build/.next ./.next -COPY --from=build /build/public ./public +RUN yarn add npm-run-all dotenv prisma -USER node +# You only need to copy next.config.js if you are NOT using the default configuration +COPY --from=builder /app/next.config.js . +COPY --from=builder --chown=nextjs:nodejs /app/public ./public +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/scripts ./scripts + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs EXPOSE 3000 -CMD ["yarn", "start"] + +ENV PORT 3000 + +CMD ["yarn", "start-docker"] diff --git a/Procfile b/Procfile deleted file mode 100644 index edc6c9a0..00000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: npm run start-env diff --git a/README.md b/README.md index 98fe2150..f29fd217 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # umami -Umami is a simple, fast, website analytics alternative to Google Analytics. +Umami is a simple, fast, privacy-focused alternative to Google Analytics. ## Getting started @@ -10,43 +10,29 @@ A detailed getting started guide can be found at [https://umami.is/docs/](https: ### Requirements -- A server with Node.js 12 or newer -- A database (MySQL or Postgresql) +- A server with Node.js version 12 or newer +- A database. Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/) databases. + +### Install Yarn + +``` +npm install -g yarn +``` ### Get the source code and install packages ``` -git clone https://github.com/mikecao/umami.git +git clone https://github.com/umami-software/umami.git cd umami -npm install +yarn install ``` -### Create database tables - -Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/). -Create a database for your Umami installation and install the tables with the included scripts. - -For MySQL: - -``` -mysql -u username -p databasename < sql/schema.mysql.sql -``` - -For Postgresql: - -``` -psql -h hostname -U username -d databasename -f sql/schema.postgresql.sql -``` - -This will also create a login account with username **admin** and password **umami**. - ### Configure umami Create an `.env` file with the following ``` -DATABASE_URL=(connection url) -HASH_SALT=(any random string) +DATABASE_URL=connection-url ``` The connection url is in the following format: @@ -56,21 +42,27 @@ postgresql://username:mypassword@localhost:5432/mydb mysql://username:mypassword@localhost:3306/mydb ``` -The `HASH_SALT` is used to generate unique values for your installation. - ### Build the application ```bash -npm run build +yarn build ``` +### Create database tables + +```bash +yarn update-db +``` + +This will also create a login account with username **admin** and password **umami**. + ### Start the application ```bash -npm start +yarn start ``` -By default this will launch the application on `http://localhost:3000`. You will need to either +By default this will launch the application on `http://localhost:3000`. You will need to either [proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly. @@ -84,12 +76,12 @@ docker-compose up Alternatively, to pull just the Umami Docker image with PostgreSQL support: ```bash -docker pull ghcr.io/mikecao/umami:postgresql-latest +docker pull docker.umami.is/umami-software/umami:postgresql-latest ``` Or with MySQL support: ```bash -docker pull ghcr.io/mikecao/umami:mysql-latest +docker pull docker.umami.is/umami-software/umami:mysql-latest ``` ## Getting updates @@ -98,8 +90,16 @@ To get the latest features, simply do a pull, install any new dependencies, and ```bash git pull -npm install -npm run build +yarn install +yarn build +yarn update-db +``` + +To update the Docker image, simply pull the new images and rebuild: + +```bash +docker-compose pull +docker-compose up --force-recreate ``` ## License diff --git a/app.json b/app.json index a27dc6fe..ba8b8c45 100644 --- a/app.json +++ b/app.json @@ -1,26 +1,16 @@ { - "name": "Umami", - "description": "Umami is a simple, fast, website analytics alternative to Google Analytics.", - "keywords": [ - "analytics", - "charts", - "statistics", - "web-analytics" - ], - "website": "https://umami.is", - "repository": "https://github.com/mikecao/umami", - "addons": [ - "heroku-postgresql" - ], - "env": { - "HASH_SALT": { - "description": "Used to generate unique values for your installation", - "required": true, - "generator": "secret" - } - }, - "scripts": { - "postdeploy": "psql $DATABASE_URL -f sql/schema.postgresql.sql" - }, - "success_url": "/" + "name": "Umami", + "description": "Umami is a simple, fast, website analytics alternative to Google Analytics.", + "keywords": ["analytics", "charts", "statistics", "web-analytics"], + "website": "https://umami.is", + "repository": "https://github.com/umami-software/umami", + "addons": ["heroku-postgresql"], + "env": { + "HASH_SALT": { + "description": "Used to generate unique values for your installation", + "required": true, + "generator": "secret" + } + }, + "success_url": "/" } diff --git a/assets/bars.svg b/assets/bars.svg index 91c83c48..fdb2d6e4 100644 --- a/assets/bars.svg +++ b/assets/bars.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/gear.svg b/assets/gear.svg new file mode 100644 index 00000000..ab97a693 --- /dev/null +++ b/assets/gear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/logo.svg b/assets/logo.svg index c80f1668..f0e52261 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,2 +1,2 @@ - - \ No newline at end of file + + diff --git a/assets/xmark.svg b/assets/xmark.svg index 6d72bf6d..340f479e 100644 --- a/assets/xmark.svg +++ b/assets/xmark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/components/common/FilterLink.js b/components/common/FilterLink.js new file mode 100644 index 00000000..459a8ae1 --- /dev/null +++ b/components/common/FilterLink.js @@ -0,0 +1,34 @@ +import React from 'react'; +import Link from 'next/link'; +import classNames from 'classnames'; +import usePageQuery from 'hooks/usePageQuery'; +import { safeDecodeURI } from 'lib/url'; +import Icon from './Icon'; +import External from 'assets/arrow-up-right-from-square.svg'; +import styles from './FilterLink.module.css'; + +export default function FilterLink({ id, value, label, externalUrl }) { + const { resolve, query } = usePageQuery(); + const active = query[id] !== undefined; + const selected = query[id] === value; + + return ( +
+ + + {safeDecodeURI(label || value)} + + + {externalUrl && ( + + } className={styles.icon} /> + + )} +
+ ); +} diff --git a/components/metrics/ReferrersTable.module.css b/components/common/FilterLink.module.css similarity index 79% rename from components/metrics/ReferrersTable.module.css rename to components/common/FilterLink.module.css index 238667f3..45b049da 100644 --- a/components/metrics/ReferrersTable.module.css +++ b/components/common/FilterLink.module.css @@ -1,19 +1,20 @@ -body .inactive { +.row { + display: flex; + align-items: center; +} + +.row .inactive { color: var(--gray500); } -body .active { +.row .active { color: var(--gray900); font-weight: 600; } -.row { - display: flex; - justify-content: space-between; -} - .row .link { display: none; + margin-left: 20px; } .row .label { diff --git a/components/common/HamburgerButton.js b/components/common/HamburgerButton.js new file mode 100644 index 00000000..501b8c95 --- /dev/null +++ b/components/common/HamburgerButton.js @@ -0,0 +1,44 @@ +import Button from 'components/common/Button'; +import XMark from 'assets/xmark.svg'; +import Bars from 'assets/bars.svg'; +import { useState } from 'react'; +import styles from './HamburgerButton.module.css'; +import MobileMenu from './MobileMenu'; +import { FormattedMessage } from 'react-intl'; + +const menuItems = [ + { + label: , + value: '/dashboard', + }, + { label: , value: '/realtime' }, + { label: , value: '/settings' }, + { + label: , + value: '/settings/profile', + }, + { label: , value: '/logout' }, +]; + +export default function HamburgerButton() { + const [active, setActive] = useState(false); + + function handleClick() { + setActive(state => !state); + } + + function handleClose() { + setActive(false); + } + + return ( + <> + diff --git a/components/common/UpdateNotice.module.css b/components/common/UpdateNotice.module.css index 52a97c3b..52bac611 100644 --- a/components/common/UpdateNotice.module.css +++ b/components/common/UpdateNotice.module.css @@ -2,12 +2,17 @@ display: flex; justify-content: center; align-items: center; - padding-top: 10px; - font-size: var(--font-size-small); - font-weight: 600; + padding-top: 20px; } .message { + font-size: var(--font-size-small); + font-weight: 600; + flex: 1; text-align: center; margin-right: 20px; } + +.buttons { + flex: 0; +} diff --git a/components/forms/AccountEditForm.js b/components/forms/AccountEditForm.js index 4b185f1a..97317aff 100644 --- a/components/forms/AccountEditForm.js +++ b/components/forms/AccountEditForm.js @@ -8,7 +8,7 @@ import FormLayout, { FormMessage, FormRow, } from 'components/layout/FormLayout'; -import usePost from 'hooks/usePost'; +import useApi from 'hooks/useApi'; const initialValues = { username: '', @@ -29,11 +29,11 @@ const validate = ({ user_id, username, password }) => { }; export default function AccountEditForm({ values, onSave, onClose }) { - const post = usePost(); + const { post } = useApi(); const [message, setMessage] = useState(); const handleSubmit = async values => { - const { ok, data } = await post('/api/account', values); + const { ok, data } = await post('/account', values); if (ok) { onSave(); diff --git a/components/forms/ChangePasswordForm.js b/components/forms/ChangePasswordForm.js index d62b5539..29f34521 100644 --- a/components/forms/ChangePasswordForm.js +++ b/components/forms/ChangePasswordForm.js @@ -8,7 +8,7 @@ import FormLayout, { FormMessage, FormRow, } from 'components/layout/FormLayout'; -import usePost from 'hooks/usePost'; +import useApi from 'hooks/useApi'; const initialValues = { current_password: '', @@ -37,11 +37,11 @@ const validate = ({ current_password, new_password, confirm_password }) => { }; export default function ChangePasswordForm({ values, onSave, onClose }) { - const post = usePost(); + const { post } = useApi(); const [message, setMessage] = useState(); const handleSubmit = async values => { - const { ok, data } = await post('/api/account/password', values); + const { ok, data } = await post('/account/password', values); if (ok) { onSave(); diff --git a/components/forms/DeleteForm.js b/components/forms/DeleteForm.js index 78600305..7fa5d6f6 100644 --- a/components/forms/DeleteForm.js +++ b/components/forms/DeleteForm.js @@ -8,7 +8,7 @@ import FormLayout, { FormMessage, FormRow, } from 'components/layout/FormLayout'; -import useDelete from 'hooks/useDelete'; +import useApi from 'hooks/useApi'; const CONFIRMATION_WORD = 'DELETE'; @@ -27,11 +27,11 @@ const validate = ({ confirmation }) => { }; export default function DeleteForm({ values, onSave, onClose }) { - const del = useDelete(); + const { del } = useApi(); const [message, setMessage] = useState(); const handleSubmit = async ({ type, id }) => { - const { ok, data } = await del(`/api/${type}/${id}`); + const { ok, data } = await del(`/${type}/${id}`); if (ok) { onSave(); diff --git a/components/forms/LoginForm.js b/components/forms/LoginForm.js index 68c2775c..3c131558 100644 --- a/components/forms/LoginForm.js +++ b/components/forms/LoginForm.js @@ -10,11 +10,12 @@ import FormLayout, { FormRow, } from 'components/layout/FormLayout'; import Icon from 'components/common/Icon'; +import useApi from 'hooks/useApi'; +import { setItem } from 'lib/web'; +import { AUTH_TOKEN } from 'lib/constants'; +import { setUser } from 'store/app'; import Logo from 'assets/logo.svg'; import styles from './LoginForm.module.css'; -import usePost from 'hooks/usePost'; -import { setItem } from 'lib/web'; -import { AUTH_TOKEN } from '../../lib/constants'; const validate = ({ username, password }) => { const errors = {}; @@ -30,12 +31,12 @@ const validate = ({ username, password }) => { }; export default function LoginForm() { - const post = usePost(); + const { post } = useApi(); const router = useRouter(); const [message, setMessage] = useState(); const handleSubmit = async ({ username, password }) => { - const { ok, status, data } = await post('/api/auth/login', { + const { ok, status, data } = await post('/auth/login', { username, password, }); @@ -43,7 +44,11 @@ export default function LoginForm() { if (ok) { setItem(AUTH_TOKEN, data.token); - return router.push('/'); + setUser(data.user); + + await router.push('/'); + + return null; } else { setMessage( status === 401 ? ( diff --git a/components/forms/ResetForm.js b/components/forms/ResetForm.js index 791039ac..924aa7b1 100644 --- a/components/forms/ResetForm.js +++ b/components/forms/ResetForm.js @@ -8,7 +8,7 @@ import FormLayout, { FormMessage, FormRow, } from 'components/layout/FormLayout'; -import usePost from 'hooks/usePost'; +import useApi from 'hooks/useApi'; const CONFIRMATION_WORD = 'RESET'; @@ -27,11 +27,11 @@ const validate = ({ confirmation }) => { }; export default function ResetForm({ values, onSave, onClose }) { - const post = usePost(); + const { post } = useApi(); const [message, setMessage] = useState(); const handleSubmit = async ({ type, id }) => { - const { ok, data } = await post(`/api/${type}/${id}/reset`); + const { ok, data } = await post(`/${type}/${id}/reset`); if (ok) { onSave(); diff --git a/components/forms/TrackingCodeForm.js b/components/forms/TrackingCodeForm.js index bfc6940d..e75260f7 100644 --- a/components/forms/TrackingCodeForm.js +++ b/components/forms/TrackingCodeForm.js @@ -4,12 +4,12 @@ import { useRouter } from 'next/router'; import Button from 'components/common/Button'; import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout'; import CopyButton from 'components/common/CopyButton'; - -const scriptName = process.env.TRACKER_SCRIPT_NAME || 'umami'; +import useConfig from 'hooks/useConfig'; export default function TrackingCodeForm({ values, onClose }) { const ref = useRef(); const { basePath } = useRouter(); + const { trackerScriptName } = useConfig(); return ( @@ -26,7 +26,9 @@ export default function TrackingCodeForm({ values, onClose }) { rows={3} cols={60} spellCheck={false} - defaultValue={``} + defaultValue={``} readOnly /> diff --git a/components/forms/WebsiteEditForm.js b/components/forms/WebsiteEditForm.js index 0be48561..64655d56 100644 --- a/components/forms/WebsiteEditForm.js +++ b/components/forms/WebsiteEditForm.js @@ -10,7 +10,7 @@ import FormLayout, { } from 'components/layout/FormLayout'; import Checkbox from 'components/common/Checkbox'; import { DOMAIN_REGEX } from 'lib/constants'; -import usePost from 'hooks/usePost'; +import useApi from 'hooks/useApi'; const initialValues = { name: '', @@ -34,11 +34,11 @@ const validate = ({ name, domain }) => { }; export default function WebsiteEditForm({ values, onSave, onClose }) { - const post = usePost(); + const { post } = useApi(); const [message, setMessage] = useState(); const handleSubmit = async values => { - const { ok, data } = await post('/api/website', values); + const { ok, data } = await post('/website', values); if (ok) { onSave(); diff --git a/components/helpers/StickyHeader.js b/components/helpers/StickyHeader.js index 477bd393..9c42e4fd 100644 --- a/components/helpers/StickyHeader.js +++ b/components/helpers/StickyHeader.js @@ -25,9 +25,8 @@ export default function StickyHeader({ } }; - checkPosition(); - if (enabled) { + checkPosition(); window.addEventListener('scroll', checkPosition); } diff --git a/components/layout/ButtonLayout.module.css b/components/layout/ButtonLayout.module.css index ef7707e4..5b979360 100644 --- a/components/layout/ButtonLayout.module.css +++ b/components/layout/ButtonLayout.module.css @@ -1,6 +1,7 @@ .buttons { display: flex; align-items: center; + width: 100%; } .buttons button + * { diff --git a/components/layout/Footer.js b/components/layout/Footer.js index 33eda5e2..c8035fef 100644 --- a/components/layout/Footer.js +++ b/components/layout/Footer.js @@ -1,36 +1,34 @@ -import React from 'react'; +import { useRouter } from 'next/router'; +import Script from 'next/script'; import classNames from 'classnames'; import { FormattedMessage } from 'react-intl'; import Link from 'components/common/Link'; import styles from './Footer.module.css'; -import useVersion from 'hooks/useVersion'; -import useLocale from 'hooks/useLocale'; +import { CURRENT_VERSION, HOMEPAGE_URL, REPO_URL } from 'lib/constants'; export default function Footer() { - const { current } = useVersion(); - const { dir } = useLocale(); + const { pathname } = useRouter(); return ( -