diff --git a/.eslintrc.json b/.eslintrc.json index 691ae90c..82f6a122 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,13 +13,6 @@ "ecmaVersion": 11, "sourceType": "module" }, - "settings": { - "import/resolver": { - "node": { - "moduleDirectory": ["node_modules", "src/"] - } - } - }, "extends": [ "plugin:@typescript-eslint/recommended", "eslint:recommended", diff --git a/Dockerfile b/Dockerfile index 824c16db..393dd9e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Install dependencies only when needed -FROM node:18-alpine AS deps +FROM node:22-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 @@ -9,7 +9,7 @@ RUN yarn config set network-timeout 300000 RUN yarn install --frozen-lockfile # Rebuild the source code only when needed -FROM node:18-alpine AS builder +FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . @@ -26,18 +26,21 @@ ENV NEXT_TELEMETRY_DISABLED 1 RUN yarn build-docker # Production image, copy all the files and run next -FROM node:18-alpine AS runner +FROM node:22-alpine AS runner WORKDIR /app +ARG NODE_OPTIONS + ENV NODE_ENV production ENV NEXT_TELEMETRY_DISABLED 1 +ENV NODE_OPTIONS $NODE_OPTIONS RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs RUN set -x \ - && apk add --no-cache curl openssl \ - && yarn add npm-run-all dotenv semver prisma@5.17.0 + && apk add --no-cache curl \ + && yarn add npm-run-all dotenv semver prisma@6.1.0 # You only need to copy next.config.js if you are NOT using the default configuration COPY --from=builder /app/next.config.js . diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma index 476e445f..2c5bec4e 100644 --- a/db/mysql/schema.prisma +++ b/db/mysql/schema.prisma @@ -1,6 +1,6 @@ generator client { provider = "prisma-client-js" - binaryTargets = ["native", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"] + binaryTargets = ["native"] } datasource db { diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index b95e4c52..8f063f8f 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -1,6 +1,6 @@ generator client { provider = "prisma-client-js" - binaryTargets = ["native", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"] + binaryTargets = ["native"] } datasource db { diff --git a/next-env.d.ts b/next-env.d.ts index 725dd6f2..40c3d680 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/next.config.js b/next.config.js index 5e3a1225..7a65c472 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ require('dotenv').config(); -const path = require('path'); const pkg = require('./package.json'); const TRACKER_SCRIPT = '/script.js'; @@ -9,6 +8,7 @@ const basePath = process.env.BASE_PATH; const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT; const cloudMode = process.env.CLOUD_MODE; const cloudUrl = process.env.CLOUD_URL; +const corsMaxAge = process.env.CORS_MAX_AGE; const defaultLocale = process.env.DEFAULT_LOCALE; const disableLogin = process.env.DISABLE_LOGIN; const disableUI = process.env.DISABLE_UI; @@ -60,6 +60,15 @@ const trackerHeaders = [ ]; const headers = [ + { + source: '/api/:path*', + headers: [ + { key: 'Access-Control-Allow-Origin', value: '*' }, + { key: 'Access-Control-Allow-Headers', value: '*' }, + { key: 'Access-Control-Allow-Methods', value: 'GET, DELETE, POST, PUT' }, + { key: 'Access-Control-Max-Age', value: corsMaxAge || '86400' }, + ], + }, { source: '/:path*', headers: defaultHeaders, @@ -169,27 +178,22 @@ const config = { typescript: { ignoreBuildErrors: true, }, + experimental: { + turbo: { + rules: { + '*.svg': { + loaders: ['@svgr/webpack'], + as: '*.js', + }, + }, + }, + }, webpack(config) { - const fileLoaderRule = config.module.rules.find(rule => rule.test?.test?.('.svg')); - - config.module.rules.push( - { - ...fileLoaderRule, - test: /\.svg$/i, - resourceQuery: /url/, - }, - { - test: /\.svg$/i, - issuer: fileLoaderRule.issuer, - resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, - use: ['@svgr/webpack'], - }, - ); - - fileLoaderRule.exclude = /\.svg$/i; - - config.resolve.alias['public'] = path.resolve('./public'); - + config.module.rules.push({ + test: /\.svg$/, + issuer: /\.(js|ts)x?$/, + use: ['@svgr/webpack'], + }); return config; }, async headers() { diff --git a/package.json b/package.json index b42831aa..275f1408 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.15.1", + "version": "2.16.0", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Umami Software, Inc. ", "license": "MIT", @@ -10,7 +10,7 @@ "url": "https://github.com/umami-software/umami.git" }, "scripts": { - "dev": "next dev -p 3000", + "dev": "next dev -p 3000 --turbo", "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "start": "next start", "build-docker": "npm-run-all build-db build-tracker build-geo build-app", @@ -63,21 +63,20 @@ "cacheDirectories": [ ".next/cache" ], - "resolutions": { - "jackspeak": "2.1.1" - }, "dependencies": { - "@clickhouse/client": "^1.4.1", + "@clickhouse/client": "^1.10.1", "@date-fns/utc": "^1.2.0", "@dicebear/collection": "^9.2.1", "@dicebear/core": "^9.2.1", "@fontsource/inter": "^4.5.15", - "@prisma/client": "5.22.0", - "@prisma/extension-read-replicas": "^0.3.0", + "@hello-pangea/dnd": "^17.0.0", + "@prisma/client": "6.1.0", + "@prisma/extension-read-replicas": "^0.4.0", "@react-spring/web": "^9.7.3", "@tanstack/react-query": "^5.28.6", "@umami/prisma-client": "^0.14.0", - "@umami/redis-client": "^0.24.0", + "@umami/redis-client": "^0.26.0", + "bcryptjs": "^2.4.3", "chalk": "^4.1.1", "chart.js": "^4.4.2", "chartjs-adapter-date-fns": "^3.0.0", @@ -99,17 +98,17 @@ "is-docker": "^3.0.0", "is-localhost-ip": "^1.4.0", "isbot": "^5.1.16", + "jsonwebtoken": "^9.0.2", "kafkajs": "^2.1.0", - "maxmind": "^4.3.6", + "maxmind": "^4.3.24", "md5": "^2.3.0", "next": "15.0.4", - "next-basics": "^0.39.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", - "prisma": "5.22.0", + "prisma": "6.1.0", + "pure-rand": "^6.1.0", "react": "^19.0.0", - "react-basics": "^0.125.0", - "react-beautiful-dnd": "^13.1.0", + "react-basics": "^0.126.0", "react-dom": "^19.0.0", "react-error-boundary": "^4.0.4", "react-intl": "^6.5.5", @@ -118,9 +117,10 @@ "react-window": "^1.8.6", "request-ip": "^3.3.0", "semver": "^7.5.4", + "serialize-error": "^12.0.0", "thenby": "^1.3.4", "uuid": "^9.0.0", - "yup": "^0.32.11", + "zod": "^3.24.1", "zustand": "^4.5.5" }, "devDependencies": { @@ -135,15 +135,16 @@ "@svgr/webpack": "^8.1.0", "@types/cypress": "^1.1.3", "@types/jest": "^29.5.14", - "@types/node": "^20.9.0", - "@types/react": "^18.2.41", - "@types/react-dom": "^18.2.17", + "@types/node": "^22.13.4", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.2", + "@types/react-intl": "^3.0.0", "@types/react-window": "^1.8.8", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", "cross-env": "^7.0.3", "cypress": "^13.6.6", - "esbuild": "^0.17.17", + "esbuild": "^0.25.0", "eslint": "^8.33.0", "eslint-config-next": "^14.0.4", "eslint-config-prettier": "^8.5.0", diff --git a/public/intl/messages/fa-IR.json b/public/intl/messages/fa-IR.json index 34b9a363..b24e4732 100644 --- a/public/intl/messages/fa-IR.json +++ b/public/intl/messages/fa-IR.json @@ -2,7 +2,7 @@ "label.access-code": [ { "type": 0, - "value": "Access code" + "value": "کد دسترسی" } ], "label.actions": [ @@ -14,31 +14,31 @@ "label.activity": [ { "type": 0, - "value": "Activity log" + "value": "فعالیت" } ], "label.add": [ { "type": 0, - "value": "Add" + "value": "افزودن" } ], "label.add-description": [ { "type": 0, - "value": "Add description" + "value": "افزودن توضیحات" } ], "label.add-member": [ { "type": 0, - "value": "Add member" + "value": "افزودن عضو" } ], "label.add-step": [ { "type": 0, - "value": "Add step" + "value": "افزودن قدم" } ], "label.add-website": [ @@ -56,7 +56,7 @@ "label.after": [ { "type": 0, - "value": "After" + "value": "بعد" } ], "label.all": [ @@ -68,55 +68,55 @@ "label.all-time": [ { "type": 0, - "value": "همه زمان" + "value": "تمامی زمان‌ها" } ], "label.analytics": [ { "type": 0, - "value": "Analytics" + "value": "تجزیه و تحلیل" } ], "label.average": [ { "type": 0, - "value": "Average" + "value": "میانگین" } ], "label.back": [ { "type": 0, - "value": "برگشت" + "value": "بازگشت" } ], "label.before": [ { "type": 0, - "value": "Before" + "value": "قبل از" } ], "label.bounce-rate": [ { "type": 0, - "value": "نرخ Bounce" + "value": "نرخ ریزش" } ], "label.breakdown": [ { "type": 0, - "value": "Breakdown" + "value": "تفکیک" } ], "label.browser": [ { "type": 0, - "value": "Browser" + "value": "مرورگر" } ], "label.browsers": [ { "type": 0, - "value": "مروگرها" + "value": "مرورگرها" } ], "label.cancel": [ @@ -134,55 +134,55 @@ "label.cities": [ { "type": 0, - "value": "Cities" + "value": "شهرها" } ], "label.city": [ { "type": 0, - "value": "City" + "value": "شهر" } ], "label.clear-all": [ { "type": 0, - "value": "Clear all" + "value": "پاک کردن همه" } ], "label.compare": [ { "type": 0, - "value": "Compare" + "value": "مقایسه" } ], "label.confirm": [ { "type": 0, - "value": "Confirm" + "value": "تأیید" } ], "label.confirm-password": [ { "type": 0, - "value": "تایید رمز" + "value": "تأیید رمز" } ], "label.contains": [ { "type": 0, - "value": "Contains" + "value": "شامل" } ], "label.continue": [ { "type": 0, - "value": "Continue" + "value": "ادامه" } ], "label.count": [ { "type": 0, - "value": "Count" + "value": "تعداد" } ], "label.countries": [ @@ -194,49 +194,49 @@ "label.country": [ { "type": 0, - "value": "Country" + "value": "کشور" } ], "label.create": [ { "type": 0, - "value": "Create" + "value": "ایجاد" } ], "label.create-report": [ { "type": 0, - "value": "Create report" + "value": "ایجاد گزارش" } ], "label.create-team": [ { "type": 0, - "value": "Create team" + "value": "ایجاد تیم" } ], "label.create-user": [ { "type": 0, - "value": "Create user" + "value": "ایجاد کاربر" } ], "label.created": [ { "type": 0, - "value": "Created" + "value": "ایجاد شد" } ], "label.created-by": [ { "type": 0, - "value": "Created By" + "value": "ایجاد شده توسط" } ], "label.current": [ { "type": 0, - "value": "Current" + "value": "فعلی" } ], "label.current-password": [ @@ -260,13 +260,13 @@ "label.data": [ { "type": 0, - "value": "Data" + "value": "داده" } ], "label.date": [ { "type": 0, - "value": "Date" + "value": "تاریخ" } ], "label.date-range": [ @@ -278,13 +278,13 @@ "label.day": [ { "type": 0, - "value": "Day" + "value": "روز" } ], "label.default-date-range": [ { "type": 0, - "value": "محدوده‌ی پیشفرض تاریخ" + "value": "محدوده‌ی پیش‌فرض تاریخ" } ], "label.delete": [ @@ -296,19 +296,19 @@ "label.delete-report": [ { "type": 0, - "value": "Delete report" + "value": "حذف گزارش" } ], "label.delete-team": [ { "type": 0, - "value": "Delete team" + "value": "حذف تیم" } ], "label.delete-user": [ { "type": 0, - "value": "Delete user" + "value": "حذف کاربر" } ], "label.delete-website": [ @@ -320,7 +320,7 @@ "label.description": [ { "type": 0, - "value": "Description" + "value": "توضیحات" } ], "label.desktop": [ @@ -332,13 +332,13 @@ "label.details": [ { "type": 0, - "value": "Details" + "value": "جزئیات" } ], "label.device": [ { "type": 0, - "value": "Device" + "value": "دستگاه" } ], "label.devices": [ @@ -356,7 +356,7 @@ "label.does-not-contain": [ { "type": 0, - "value": "Does not contain" + "value": "شامل نمی‌شود" } ], "label.domain": [ @@ -368,7 +368,7 @@ "label.dropoff": [ { "type": 0, - "value": "Dropoff" + "value": "رها کردن" } ], "label.edit": [ @@ -380,43 +380,43 @@ "label.edit-dashboard": [ { "type": 0, - "value": "Edit dashboard" + "value": "ویرایش داشبورد" } ], "label.edit-member": [ { "type": 0, - "value": "Edit member" + "value": "ویرایش عضو" } ], "label.enable-share-url": [ { "type": 0, - "value": "فعال کردن اشتراک گذاری URL" + "value": "فعال کردن اشتراک گذاری آدرس اینترنتی" } ], "label.end-step": [ { "type": 0, - "value": "End Step" + "value": "قدم پایانی" } ], "label.entry": [ { "type": 0, - "value": "Entry URL" + "value": "آدرس اینترنتی ورودی" } ], "label.event": [ { "type": 0, - "value": "Event" + "value": "رویداد" } ], "label.event-data": [ { "type": 0, - "value": "Event data" + "value": "داده‌های رویداد" } ], "label.events": [ @@ -428,31 +428,31 @@ "label.exit": [ { "type": 0, - "value": "Exit URL" + "value": "آدرس اینترنتی خروجی" } ], "label.false": [ { "type": 0, - "value": "False" + "value": "نادرست" } ], "label.field": [ { "type": 0, - "value": "Field" + "value": "فیلد" } ], "label.fields": [ { "type": 0, - "value": "Fields" + "value": "فیلد‌ها" } ], "label.filter": [ { "type": 0, - "value": "Filter" + "value": "فیلتر" } ], "label.filter-combined": [ @@ -470,127 +470,127 @@ "label.filters": [ { "type": 0, - "value": "Filters" + "value": "فیلترها" } ], "label.first-seen": [ { "type": 0, - "value": "First seen" + "value": "اولین بار دیده شده" } ], "label.funnel": [ { "type": 0, - "value": "Funnel" + "value": "فانل" } ], "label.funnel-description": [ { "type": 0, - "value": "Understand the conversion and drop-off rate of users." + "value": "نرخ تبدیل و رها کردن کاربران را درک کنید." } ], "label.goal": [ { "type": 0, - "value": "Goal" + "value": "هدف" } ], "label.goals": [ { "type": 0, - "value": "Goals" + "value": "اهداف" } ], "label.goals-description": [ { "type": 0, - "value": "Track your goals for pageviews and events." + "value": "اهداف خود را برای بازدید از صفحه و رویدادها دنبال کنید." } ], "label.greater-than": [ { "type": 0, - "value": "Greater than" + "value": "بزرگ‌تر از" } ], "label.greater-than-equals": [ { "type": 0, - "value": "Greater than or equals" + "value": "بزرگ‌تر یا مساوی" } ], "label.host": [ { "type": 0, - "value": "Host" + "value": "هاست" } ], "label.hosts": [ { "type": 0, - "value": "Hosts" + "value": "هاست‌ها" } ], "label.insights": [ { "type": 0, - "value": "Insights" + "value": "بینش" } ], "label.insights-description": [ { "type": 0, - "value": "Dive deeper into your data by using segments and filters." + "value": "با استفاده از بخش‌ها و فیلترها، در داده‌های خود عمیق‌تر شوید." } ], "label.is": [ { "type": 0, - "value": "Is" + "value": "برابر است با" } ], "label.is-not": [ { "type": 0, - "value": "Is not" + "value": "برابر نیست با" } ], "label.is-not-set": [ { "type": 0, - "value": "Is not set" + "value": "تعیین نشده" } ], "label.is-set": [ { "type": 0, - "value": "Is set" + "value": "تعیین شده" } ], "label.join": [ { "type": 0, - "value": "Join" + "value": "پیوستن" } ], "label.join-team": [ { "type": 0, - "value": "Join team" + "value": "پیوستن به تیم" } ], "label.journey": [ { "type": 0, - "value": "Journey" + "value": "مسیر" } ], "label.journey-description": [ { "type": 0, - "value": "Understand how users navigate through your website." + "value": "درک کنید که کاربران چگونه در وب‌سایت شما حرکت می کنند." } ], "label.language": [ @@ -612,10 +612,6 @@ } ], "label.last-days": [ - { - "type": 0, - "value": "لیست " - }, { "type": 1, "value": "x" @@ -626,10 +622,6 @@ } ], "label.last-hours": [ - { - "type": 0, - "value": "لیست " - }, { "type": 1, "value": "x" @@ -640,47 +632,43 @@ } ], "label.last-months": [ - { - "type": 0, - "value": "Last " - }, { "type": 1, "value": "x" }, { "type": 0, - "value": " months" + "value": " ماه گذشته" } ], "label.last-seen": [ { "type": 0, - "value": "Last seen" + "value": "آخرین بار دیده شده" } ], "label.leave": [ { "type": 0, - "value": "Leave" + "value": "ترک کردن" } ], "label.leave-team": [ { "type": 0, - "value": "Leave team" + "value": "ترک تیم" } ], "label.less-than": [ { "type": 0, - "value": "Less than" + "value": "کمتر از" } ], "label.less-than-equals": [ { "type": 0, - "value": "Less than or equals" + "value": "کمتر یا مساوی" } ], "label.login": [ @@ -698,37 +686,37 @@ "label.manage": [ { "type": 0, - "value": "Manage" + "value": "مدیریت" } ], "label.manager": [ { "type": 0, - "value": "Manager" + "value": "مدیر" } ], "label.max": [ { "type": 0, - "value": "Max" + "value": "حداکثر" } ], "label.member": [ { "type": 0, - "value": "Member" + "value": "عضو" } ], "label.members": [ { "type": 0, - "value": "Members" + "value": "اعضا" } ], "label.min": [ { "type": 0, - "value": "Min" + "value": "حداقل" } ], "label.mobile": [ @@ -746,13 +734,13 @@ "label.my-account": [ { "type": 0, - "value": "My account" + "value": "حساب کاربری من" } ], "label.my-websites": [ { "type": 0, - "value": "My websites" + "value": "وب‌سایت‌های من" } ], "label.name": [ @@ -770,7 +758,7 @@ "label.none": [ { "type": 0, - "value": "None" + "value": "هیچ" } ], "label.number-of-records": [ @@ -810,31 +798,31 @@ "label.ok": [ { "type": 0, - "value": "OK" + "value": "تایید" } ], "label.os": [ { "type": 0, - "value": "OS" + "value": "سیستم عامل" } ], "label.overview": [ { "type": 0, - "value": "Overview" + "value": "بررسی کلی" } ], "label.owner": [ { "type": 0, - "value": "ایجاد شده توسط" + "value": "مالک" } ], "label.page-of": [ { "type": 0, - "value": "Page " + "value": "صفحه " }, { "type": 1, @@ -842,7 +830,7 @@ }, { "type": 0, - "value": " of " + "value": " از " }, { "type": 1, @@ -858,7 +846,7 @@ "label.pageTitle": [ { "type": 0, - "value": "Page title" + "value": "عنوان صفحه" } ], "label.pages": [ @@ -876,13 +864,13 @@ "label.path": [ { "type": 0, - "value": "Path" + "value": "مسیر" } ], "label.paths": [ { "type": 0, - "value": "Paths" + "value": "مسیرها" } ], "label.powered-by": [ @@ -898,19 +886,19 @@ "label.previous": [ { "type": 0, - "value": "Previous" + "value": "قبلی" } ], "label.previous-period": [ { "type": 0, - "value": "Previous period" + "value": "دوره‌ی قبل" } ], "label.previous-year": [ { "type": 0, - "value": "Previous year" + "value": "سال قبل" } ], "label.profile": [ @@ -922,31 +910,31 @@ "label.properties": [ { "type": 0, - "value": "Properties" + "value": "ویژگی‌ها" } ], "label.property": [ { "type": 0, - "value": "Property" + "value": "ویژگی" } ], "label.queries": [ { "type": 0, - "value": "Queries" + "value": "کوئری‌ها" } ], "label.query": [ { "type": 0, - "value": "Query" + "value": "کوئری" } ], "label.query-parameters": [ { "type": 0, - "value": "Query parameters" + "value": "پارامترهای کوئری" } ], "label.realtime": [ @@ -958,7 +946,7 @@ "label.referrer": [ { "type": 0, - "value": "Referrer" + "value": "ارجاع دهنده" } ], "label.referrers": [ @@ -976,37 +964,37 @@ "label.regenerate": [ { "type": 0, - "value": "Regenerate" + "value": "تولید مجدد" } ], "label.region": [ { "type": 0, - "value": "Region" + "value": "منطقه" } ], "label.regions": [ { "type": 0, - "value": "Regions" + "value": "مناطق" } ], "label.remove": [ { "type": 0, - "value": "Remove" + "value": "حذف" } ], "label.remove-member": [ { "type": 0, - "value": "Remove member" + "value": "حذف عضو" } ], "label.reports": [ { "type": 0, - "value": "Reports" + "value": "گزارش‌ها" } ], "label.required": [ @@ -1024,49 +1012,49 @@ "label.reset-website": [ { "type": 0, - "value": "بازنشانی آمار" + "value": "بازنشانی وب‌سایت" } ], "label.retention": [ { "type": 0, - "value": "Retention" + "value": "نرخ بازگشت" } ], "label.retention-description": [ { "type": 0, - "value": "Measure your website stickiness by tracking how often users return." + "value": "چسبندگی وب‌سایت خود را با دنبال کردن تعداد دفعات بازگشت کاربران اندازه‌گیری کنید." } ], "label.revenue": [ { "type": 0, - "value": "Revenue" + "value": "درآمد" } ], "label.revenue-description": [ { "type": 0, - "value": "Look into your revenue across time." + "value": "به درآمد خود در طول زمان نگاه کنید." } ], "label.revenue-property": [ { "type": 0, - "value": "Revenue Property" + "value": "ویژگی درآمد" } ], "label.role": [ { "type": 0, - "value": "Role" + "value": "نقش" } ], "label.run-query": [ { "type": 0, - "value": "Run query" + "value": "اجرای کوئری" } ], "label.save": [ @@ -1078,49 +1066,49 @@ "label.screens": [ { "type": 0, - "value": "Screens" + "value": "صفحه" } ], "label.search": [ { "type": 0, - "value": "Search" + "value": "جستجو" } ], "label.select": [ { "type": 0, - "value": "Select" + "value": "انتخاب" } ], "label.select-date": [ { "type": 0, - "value": "Select date" + "value": "انتخاب تاریخ" } ], "label.select-role": [ { "type": 0, - "value": "Select role" + "value": "انتخاب نقش" } ], "label.select-website": [ { "type": 0, - "value": "Select website" + "value": "انتخاب وب‌سایت" } ], "label.session": [ { "type": 0, - "value": "Session" + "value": "نشست" } ], "label.sessions": [ { "type": 0, - "value": "Sessions" + "value": "نشست‌ها" } ], "label.settings": [ @@ -1132,7 +1120,7 @@ "label.share-url": [ { "type": 0, - "value": "به اشتراک گذاری URL" + "value": "به اشتراک گذاری آدرس اینترنتی" } ], "label.single-day": [ @@ -1144,19 +1132,19 @@ "label.start-step": [ { "type": 0, - "value": "Start Step" + "value": "قدم شروع" } ], "label.steps": [ { "type": 0, - "value": "Steps" + "value": "قدم‌ها" } ], "label.sum": [ { "type": 0, - "value": "Sum" + "value": "جمع" } ], "label.tablet": [ @@ -1168,55 +1156,55 @@ "label.team": [ { "type": 0, - "value": "Team" + "value": "تیم" } ], "label.team-id": [ { "type": 0, - "value": "Team ID" + "value": "شناسه تیم" } ], "label.team-manager": [ { "type": 0, - "value": "Team manager" + "value": "مدیر تیم" } ], "label.team-member": [ { "type": 0, - "value": "Team member" + "value": "عضو تیم" } ], "label.team-name": [ { "type": 0, - "value": "Team name" + "value": "نام تیم" } ], "label.team-owner": [ { "type": 0, - "value": "Team owner" + "value": "مالک تیم" } ], "label.team-view-only": [ { "type": 0, - "value": "Team view only" + "value": "فقط مشاهده‌ی تیم" } ], "label.team-websites": [ { "type": 0, - "value": "Team websites" + "value": "وب‌سایت‌های تیم" } ], "label.teams": [ { "type": 0, - "value": "Teams" + "value": "تیم‌ها" } ], "label.theme": [ @@ -1252,7 +1240,7 @@ "label.title": [ { "type": 0, - "value": "Title" + "value": "عنوان" } ], "label.today": [ @@ -1264,19 +1252,19 @@ "label.toggle-charts": [ { "type": 0, - "value": "Toggle charts" + "value": "نمایش / عدم نمایش نمودارها" } ], "label.total": [ { "type": 0, - "value": "Total" + "value": "جمع" } ], "label.total-records": [ { "type": 0, - "value": "Total records" + "value": "جمع رکوردها" } ], "label.tracking-code": [ @@ -1288,37 +1276,37 @@ "label.transactions": [ { "type": 0, - "value": "Transactions" + "value": "تراکنش‌ها" } ], "label.transfer": [ { "type": 0, - "value": "Transfer" + "value": "انتقال" } ], "label.transfer-website": [ { "type": 0, - "value": "Transfer website" + "value": "انتقال وب‌سایت" } ], "label.true": [ { "type": 0, - "value": "True" + "value": "درست" } ], "label.type": [ { "type": 0, - "value": "Type" + "value": "نوع" } ], "label.unique": [ { "type": 0, - "value": "Unique" + "value": "یکتا" } ], "label.unique-visitors": [ @@ -1330,7 +1318,7 @@ "label.uniqueCustomers": [ { "type": 0, - "value": "Unique Customers" + "value": "مشتریان یکتا" } ], "label.unknown": [ @@ -1342,37 +1330,37 @@ "label.untitled": [ { "type": 0, - "value": "Untitled" + "value": "بدون عنوان" } ], "label.update": [ { "type": 0, - "value": "Update" + "value": "به‌روزرسانی" } ], "label.url": [ { "type": 0, - "value": "URL" + "value": "آدرس اینترنتی" } ], "label.urls": [ { "type": 0, - "value": "URLs" + "value": "آدرس‌های اینترنتی" } ], "label.user": [ { "type": 0, - "value": "User" + "value": "کاربر" } ], "label.user-property": [ { "type": 0, - "value": "User Property" + "value": "ویژگی کاربر" } ], "label.username": [ @@ -1384,7 +1372,7 @@ "label.users": [ { "type": 0, - "value": "Users" + "value": "کاربران" } ], "label.utm": [ @@ -1396,19 +1384,19 @@ "label.utm-description": [ { "type": 0, - "value": "Track your campaigns through UTM parameters." + "value": "با استفاده از پارامترهای UTM، کمپین‌های خود را بررسی کنید." } ], "label.value": [ { "type": 0, - "value": "Value" + "value": "مقدار" } ], "label.view": [ { "type": 0, - "value": "View" + "value": "مشاهده" } ], "label.view-details": [ @@ -1420,7 +1408,7 @@ "label.view-only": [ { "type": 0, - "value": "View only" + "value": "فقط مشاهده" } ], "label.views": [ @@ -1432,7 +1420,7 @@ "label.views-per-visit": [ { "type": 0, - "value": "Views per visit" + "value": "نمایش‌ها در هر بازدید" } ], "label.visit-duration": [ @@ -1450,19 +1438,19 @@ "label.visits": [ { "type": 0, - "value": "Visits" + "value": "بازدیدها" } ], "label.website": [ { "type": 0, - "value": "Website" + "value": "وب‌سایت" } ], "label.website-id": [ { "type": 0, - "value": "Website ID" + "value": "شناسه وب‌سایت" } ], "label.websites": [ @@ -1474,19 +1462,19 @@ "label.window": [ { "type": 0, - "value": "Window" + "value": "پنجره" } ], "label.yesterday": [ { "type": 0, - "value": "Yesterday" + "value": "دیروز" } ], "message.action-confirmation": [ { "type": 0, - "value": "Type " + "value": "برای تأیید این عملیات، لطفاً " }, { "type": 1, @@ -1494,7 +1482,7 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " را تایپ کنید." } ], "message.active-users": [ @@ -1504,7 +1492,7 @@ }, { "type": 0, - "value": " هم اکنون " + "value": " فعلی " }, { "offset": 0, @@ -1534,7 +1522,7 @@ "message.collected-data": [ { "type": 0, - "value": "Collected data" + "value": "داده‌های جمع‌آوری شده" } ], "message.confirm-delete": [ @@ -1548,13 +1536,13 @@ }, { "type": 0, - "value": " را حذف کنید?" + "value": " را حذف کنید؟" } ], "message.confirm-leave": [ { "type": 0, - "value": "Are you sure you want to leave " + "value": "آیا مطمئن هستید می‌خواهید از " }, { "type": 1, @@ -1562,13 +1550,13 @@ }, { "type": 0, - "value": "?" + "value": " خارج شوید؟" } ], "message.confirm-remove": [ { "type": 0, - "value": "Are you sure you want to remove " + "value": "آیا مطمئن هستید می‌خواهید " }, { "type": 1, @@ -1576,13 +1564,13 @@ }, { "type": 0, - "value": "?" + "value": " را حذف کنید؟" } ], "message.confirm-reset": [ { "type": 0, - "value": "آیا از بازنشانی آمار " + "value": "آیا مطمئن هستید می‌خواهید " }, { "type": 1, @@ -1590,19 +1578,19 @@ }, { "type": 0, - "value": " مطمئن هستید?" + "value": " را بازنشانی کنید؟" } ], "message.delete-team-warning": [ { "type": 0, - "value": "Deleting a team will also delete all team websites." + "value": "با حذف تیم، تمامی وب‌سایت‌های تیم هم حذف خواهند شد." } ], "message.delete-website-warning": [ { "type": 0, - "value": "همه‌ی داده‌های مرتبط هم حذف خواهد شد." + "value": "همه‌ی داده‌های وب‌سایت هم حذف خواهد شد." } ], "message.error": [ @@ -1618,7 +1606,7 @@ }, { "type": 0, - "value": " on " + "value": " در " }, { "type": 1, @@ -1640,13 +1628,13 @@ "message.invalid-domain": [ { "type": 0, - "value": "دامنه‌ی نامعتبر" + "value": "دامنه نامعتبر است." } ], "message.min-password-length": [ { "type": 0, - "value": "Minimum length of " + "value": "حداقل طول " }, { "type": 1, @@ -1654,13 +1642,13 @@ }, { "type": 0, - "value": " characters" + "value": " کاراکتر است." } ], "message.new-version-available": [ { "type": 0, - "value": "A new version of Umami " + "value": "نسخه‌ی جدیدی از Umami " }, { "type": 1, @@ -1668,7 +1656,7 @@ }, { "type": 0, - "value": " is available!" + "value": " در دسترس است." } ], "message.no-data-available": [ @@ -1680,7 +1668,7 @@ "message.no-event-data": [ { "type": 0, - "value": "No event data is available." + "value": "هیچ داده‌ای برای این رویداد وجود ندارد." } ], "message.no-match-password": [ @@ -1692,25 +1680,25 @@ "message.no-results-found": [ { "type": 0, - "value": "No results were found." + "value": "نتیجه‌ای یافت نشد." } ], "message.no-team-websites": [ { "type": 0, - "value": "This team does not have any websites." + "value": "هیچ وب‌سایتی برای این تیم وجود ندارد." } ], "message.no-teams": [ { "type": 0, - "value": "You have not created any teams." + "value": "شما هیچ تیمی را ایجاد نکرده‌اید." } ], "message.no-users": [ { "type": 0, - "value": "There are no users." + "value": "هیچ کاربری وجود ندارد." } ], "message.no-websites-configured": [ @@ -1728,7 +1716,7 @@ "message.reset-website": [ { "type": 0, - "value": "To reset this website, type " + "value": "برای بازنشانی وب‌سایت، لطفاً " }, { "type": 1, @@ -1736,51 +1724,43 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " را تایپ کنید." } ], "message.reset-website-warning": [ { "type": 0, - "value": "تمامی آمارهای این وب‌سایت حذف خواهد شد اما tracking code بدون تغییر باقی می‌ماند." + "value": "تمامی آمارهای این وب‌سایت حذف خواهد شد اما کدهای رهگیری بدون تغییر باقی می‌ماند." } ], "message.saved": [ { "type": 0, - "value": "با موفقیت ذخیره شد." + "value": "ذخیره شد." } ], "message.share-url": [ { "type": 0, - "value": "این URL به اشتراک گذاشته شده عمومی برای " - }, - { - "type": 1, - "value": "target" - }, - { - "type": 0, - "value": " است." + "value": "آمار وب‌سایت شما به صورت عمومی در آدرس زیر قابل مشاهده است." } ], "message.team-already-member": [ { "type": 0, - "value": "You are already a member of the team." + "value": "شما از قبل عضو این تیم هستید." } ], "message.team-not-found": [ { "type": 0, - "value": "Team not found." + "value": "تیم یافت نشد." } ], "message.team-websites-info": [ { "type": 0, - "value": "Websites can be viewed by anyone on the team." + "value": "وب‌سایت‌ها توسط تمامی اعضای تیم قابل مشاهده هستند." } ], "message.tracking-code": [ @@ -1792,37 +1772,37 @@ "message.transfer-team-website-to-user": [ { "type": 0, - "value": "Transfer this website to your account?" + "value": "آیا می‌خواهید این وب‌سایت را به حساب خود منتقل کنید؟" } ], "message.transfer-user-website-to-team": [ { "type": 0, - "value": "Select the team to transfer this website to." + "value": "تیم مورد نظر را برای انتقال وب‌سایت انتخاب کنید." } ], "message.transfer-website": [ { "type": 0, - "value": "Transfer website ownership to your account or another team." + "value": "مالکیت وب‌سایت را به حساب خودت یا یک تیم دیگر منتقل کنید." } ], "message.triggered-event": [ { "type": 0, - "value": "Triggered event" + "value": "رویداد فعال شده" } ], "message.user-deleted": [ { "type": 0, - "value": "User deleted." + "value": "کاربر حذف شد." } ], "message.viewed-page": [ { "type": 0, - "value": "Viewed page" + "value": "صفحه مشاهده شد" } ], "message.visitor-log": [ @@ -1862,7 +1842,7 @@ "message.visitors-dropped-off": [ { "type": 0, - "value": "Visitors dropped off" + "value": "ریزش بازدیدکننده‌ها" } ] } diff --git a/public/intl/messages/ga-ES.json b/public/intl/messages/ga-ES.json index 6ce820e6..9e2c366e 100644 --- a/public/intl/messages/ga-ES.json +++ b/public/intl/messages/ga-ES.json @@ -2,7 +2,7 @@ "label.access-code": [ { "type": 0, - "value": "Access code" + "value": "Código de acceso" } ], "label.actions": [ @@ -14,31 +14,31 @@ "label.activity": [ { "type": 0, - "value": "Activity log" + "value": "Rexistro de actividade" } ], "label.add": [ { "type": 0, - "value": "Add" + "value": "Engadir" } ], "label.add-description": [ { "type": 0, - "value": "Add description" + "value": "Engadir descrición" } ], "label.add-member": [ { "type": 0, - "value": "Add member" + "value": "Engadir membro" } ], "label.add-step": [ { "type": 0, - "value": "Add step" + "value": "Engadir paso" } ], "label.add-website": [ @@ -50,7 +50,7 @@ "label.admin": [ { "type": 0, - "value": "Administradora" + "value": "Administrador/a" } ], "label.after": [ @@ -74,13 +74,13 @@ "label.analytics": [ { "type": 0, - "value": "Analytics" + "value": "Analíticas" } ], "label.average": [ { "type": 0, - "value": "Average" + "value": "Media" } ], "label.back": [ @@ -92,7 +92,7 @@ "label.before": [ { "type": 0, - "value": "Before" + "value": "Antes" } ], "label.bounce-rate": [ @@ -104,13 +104,13 @@ "label.breakdown": [ { "type": 0, - "value": "Breakdown" + "value": "Desglose" } ], "label.browser": [ { "type": 0, - "value": "Browser" + "value": "Navegador" } ], "label.browsers": [ @@ -134,13 +134,13 @@ "label.cities": [ { "type": 0, - "value": "Cities" + "value": "Cidades" } ], "label.city": [ { "type": 0, - "value": "City" + "value": "Cidade" } ], "label.clear-all": [ @@ -152,13 +152,13 @@ "label.compare": [ { "type": 0, - "value": "Compare" + "value": "Comparar" } ], "label.confirm": [ { "type": 0, - "value": "Confirm" + "value": "Confirmar" } ], "label.confirm-password": [ @@ -170,19 +170,19 @@ "label.contains": [ { "type": 0, - "value": "Contains" + "value": "Contén" } ], "label.continue": [ { "type": 0, - "value": "Continue" + "value": "Continuar" } ], "label.count": [ { "type": 0, - "value": "Count" + "value": "Reconto" } ], "label.countries": [ @@ -194,49 +194,49 @@ "label.country": [ { "type": 0, - "value": "Country" + "value": "País" } ], "label.create": [ { "type": 0, - "value": "Create" + "value": "Crear" } ], "label.create-report": [ { "type": 0, - "value": "Create report" + "value": "Crear report" } ], "label.create-team": [ { "type": 0, - "value": "Create team" + "value": "Crear team" } ], "label.create-user": [ { "type": 0, - "value": "Create user" + "value": "Crear user" } ], "label.created": [ { "type": 0, - "value": "Created" + "value": "Creado" } ], "label.created-by": [ { "type": 0, - "value": "Created By" + "value": "Creado por" } ], "label.current": [ { "type": 0, - "value": "Current" + "value": "Actual" } ], "label.current-password": [ @@ -260,13 +260,13 @@ "label.data": [ { "type": 0, - "value": "Data" + "value": "Datos" } ], "label.date": [ { "type": 0, - "value": "Date" + "value": "Data" } ], "label.date-range": [ @@ -278,7 +278,7 @@ "label.day": [ { "type": 0, - "value": "Day" + "value": "Día" } ], "label.default-date-range": [ @@ -296,19 +296,19 @@ "label.delete-report": [ { "type": 0, - "value": "Delete report" + "value": "Eliminar reporte" } ], "label.delete-team": [ { "type": 0, - "value": "Delete team" + "value": "Eliminar equipo" } ], "label.delete-user": [ { "type": 0, - "value": "Delete user" + "value": "Eliminar usuario" } ], "label.delete-website": [ @@ -320,7 +320,7 @@ "label.description": [ { "type": 0, - "value": "Description" + "value": "Descripción" } ], "label.desktop": [ @@ -332,13 +332,13 @@ "label.details": [ { "type": 0, - "value": "Details" + "value": "Detalles" } ], "label.device": [ { "type": 0, - "value": "Device" + "value": "Dispositivo" } ], "label.devices": [ @@ -356,7 +356,7 @@ "label.does-not-contain": [ { "type": 0, - "value": "Does not contain" + "value": "Non contén" } ], "label.domain": [ @@ -368,7 +368,7 @@ "label.dropoff": [ { "type": 0, - "value": "Dropoff" + "value": "Disminución" } ], "label.edit": [ @@ -380,13 +380,13 @@ "label.edit-dashboard": [ { "type": 0, - "value": "Edit dashboard" + "value": "Editar taboleiro" } ], "label.edit-member": [ { "type": 0, - "value": "Edit member" + "value": "Editar membro" } ], "label.enable-share-url": [ @@ -410,13 +410,13 @@ "label.event": [ { "type": 0, - "value": "Event" + "value": "Evento" } ], "label.event-data": [ { "type": 0, - "value": "Event data" + "value": "Datos do evento" } ], "label.events": [ @@ -428,31 +428,31 @@ "label.exit": [ { "type": 0, - "value": "Exit URL" + "value": "URL de saída" } ], "label.false": [ { "type": 0, - "value": "False" + "value": "Falso" } ], "label.field": [ { "type": 0, - "value": "Field" + "value": "Campo" } ], "label.fields": [ { "type": 0, - "value": "Fields" + "value": "Campos" } ], "label.filter": [ { "type": 0, - "value": "Filter" + "value": "Filtro" } ], "label.filter-combined": [ @@ -464,19 +464,19 @@ "label.filter-raw": [ { "type": 0, - "value": "Raw" + "value": "Crú" } ], "label.filters": [ { "type": 0, - "value": "Filters" + "value": "Filtros" } ], "label.first-seen": [ { "type": 0, - "value": "First seen" + "value": "Primeira visita" } ], "label.funnel": [ @@ -488,49 +488,49 @@ "label.funnel-description": [ { "type": 0, - "value": "Understand the conversion and drop-off rate of users." + "value": "Entende a taxa de conversión e de abandono dos usuarios." } ], "label.goal": [ { "type": 0, - "value": "Goal" + "value": "Obxectivo" } ], "label.goals": [ { "type": 0, - "value": "Goals" + "value": "Obxectivos" } ], "label.goals-description": [ { "type": 0, - "value": "Track your goals for pageviews and events." + "value": "Segue os teus obxectivos de visualizacións de páxinas e eventos." } ], "label.greater-than": [ { "type": 0, - "value": "Greater than" + "value": "Maior que" } ], "label.greater-than-equals": [ { "type": 0, - "value": "Greater than or equals" + "value": "Maior ou igual que" } ], "label.host": [ { "type": 0, - "value": "Host" + "value": "Dominio" } ], "label.hosts": [ { "type": 0, - "value": "Hosts" + "value": "Dominios" } ], "label.insights": [ @@ -548,49 +548,49 @@ "label.is": [ { "type": 0, - "value": "Is" + "value": "É" } ], "label.is-not": [ { "type": 0, - "value": "Is not" + "value": "Non é" } ], "label.is-not-set": [ { "type": 0, - "value": "Is not set" + "value": "Non está establecido" } ], "label.is-set": [ { "type": 0, - "value": "Is set" + "value": "Está establecido" } ], "label.join": [ { "type": 0, - "value": "Join" + "value": "Unirse" } ], "label.join-team": [ { "type": 0, - "value": "Join team" + "value": "Unirse ao equipo" } ], "label.journey": [ { "type": 0, - "value": "Journey" + "value": "Traxectoria" } ], "label.journey-description": [ { "type": 0, - "value": "Understand how users navigate through your website." + "value": "Entende como os usuarios navegan polo teu sitio web." } ], "label.language": [ @@ -642,7 +642,7 @@ "label.last-months": [ { "type": 0, - "value": "Last " + "value": "Últimos " }, { "type": 1, @@ -650,37 +650,37 @@ }, { "type": 0, - "value": " months" + "value": " meses" } ], "label.last-seen": [ { "type": 0, - "value": "Last seen" + "value": "Última visita" } ], "label.leave": [ { "type": 0, - "value": "Leave" + "value": "Deixar" } ], "label.leave-team": [ { "type": 0, - "value": "Leave team" + "value": "Deixar o equipo" } ], "label.less-than": [ { "type": 0, - "value": "Less than" + "value": "Menor que" } ], "label.less-than-equals": [ { "type": 0, - "value": "Less than or equals" + "value": "Menor ou igual que" } ], "label.login": [ @@ -698,13 +698,13 @@ "label.manage": [ { "type": 0, - "value": "Manage" + "value": "Xestionar" } ], "label.manager": [ { "type": 0, - "value": "Manager" + "value": "Xestor" } ], "label.max": [ @@ -716,13 +716,13 @@ "label.member": [ { "type": 0, - "value": "Member" + "value": "Membro" } ], "label.members": [ { "type": 0, - "value": "Members" + "value": "Membros" } ], "label.min": [ @@ -746,13 +746,13 @@ "label.my-account": [ { "type": 0, - "value": "My account" + "value": "A miña conta" } ], "label.my-websites": [ { "type": 0, - "value": "My websites" + "value": "Os meus sitios web" } ], "label.name": [ @@ -816,25 +816,25 @@ "label.os": [ { "type": 0, - "value": "OS" + "value": "Sistema operativo" } ], "label.overview": [ { "type": 0, - "value": "Overview" + "value": "Resumo" } ], "label.owner": [ { "type": 0, - "value": "Dona" + "value": "Propietario/a" } ], "label.page-of": [ { "type": 0, - "value": "Page " + "value": "Páxina " }, { "type": 1, @@ -842,7 +842,7 @@ }, { "type": 0, - "value": " of " + "value": " de " }, { "type": 1, @@ -858,7 +858,7 @@ "label.pageTitle": [ { "type": 0, - "value": "Page title" + "value": "Título da páxina" } ], "label.pages": [ @@ -876,13 +876,13 @@ "label.path": [ { "type": 0, - "value": "Path" + "value": "Ruta" } ], "label.paths": [ { "type": 0, - "value": "Paths" + "value": "Rutas" } ], "label.powered-by": [ @@ -898,19 +898,19 @@ "label.previous": [ { "type": 0, - "value": "Previous" + "value": "Anterior" } ], "label.previous-period": [ { "type": 0, - "value": "Previous period" + "value": "Periodo anterior" } ], "label.previous-year": [ { "type": 0, - "value": "Previous year" + "value": "Ano anterior" } ], "label.profile": [ @@ -922,31 +922,31 @@ "label.properties": [ { "type": 0, - "value": "Properties" + "value": "Propiedades" } ], "label.property": [ { "type": 0, - "value": "Property" + "value": "Propiedade" } ], "label.queries": [ { "type": 0, - "value": "Queries" + "value": "Peticións" } ], "label.query": [ { "type": 0, - "value": "Query" + "value": "Petición" } ], "label.query-parameters": [ { "type": 0, - "value": "Query parameters" + "value": "Parámetros da petición" } ], "label.realtime": [ @@ -958,7 +958,7 @@ "label.referrer": [ { "type": 0, - "value": "Referrer" + "value": "Orixe" } ], "label.referrers": [ @@ -976,37 +976,37 @@ "label.regenerate": [ { "type": 0, - "value": "Regenerate" + "value": "Rexenerar" } ], "label.region": [ { "type": 0, - "value": "Region" + "value": "Rexión" } ], "label.regions": [ { "type": 0, - "value": "Regions" + "value": "Rexións" } ], "label.remove": [ { "type": 0, - "value": "Remove" + "value": "Eliminar" } ], "label.remove-member": [ { "type": 0, - "value": "Remove member" + "value": "Eliminar membro" } ], "label.reports": [ { "type": 0, - "value": "Reports" + "value": "Reportes" } ], "label.required": [ @@ -1024,7 +1024,7 @@ "label.reset-website": [ { "type": 0, - "value": "To reset this website, type " + "value": "Para restablecer este sitio web, escriba " }, { "type": 1, @@ -1032,31 +1032,31 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " na caixa de texto de embaixo para confirmar." } ], "label.retention": [ { "type": 0, - "value": "Retention" + "value": "Retención" } ], "label.retention-description": [ { "type": 0, - "value": "Measure your website stickiness by tracking how often users return." + "value": "Mide a fidelidade dos usuarios ao teu sitio web seguindo a frecuencia coa que volven." } ], "label.revenue": [ { "type": 0, - "value": "Revenue" + "value": "Ingresos" } ], "label.revenue-description": [ { "type": 0, - "value": "Look into your revenue across time." + "value": "Consulta os teus ingresos ao longo do tempo." } ], "label.revenue-property": [ @@ -1068,13 +1068,13 @@ "label.role": [ { "type": 0, - "value": "Role" + "value": "Rol" } ], "label.run-query": [ { "type": 0, - "value": "Run query" + "value": "Executar petición" } ], "label.save": [ @@ -1086,49 +1086,49 @@ "label.screens": [ { "type": 0, - "value": "Screens" + "value": "Pantallas" } ], "label.search": [ { "type": 0, - "value": "Search" + "value": "Buscar" } ], "label.select": [ { "type": 0, - "value": "Select" + "value": "Seleccionar" } ], "label.select-date": [ { "type": 0, - "value": "Select date" + "value": "Seleccionar data" } ], "label.select-role": [ { "type": 0, - "value": "Select role" + "value": "Seleccionar rol" } ], "label.select-website": [ { "type": 0, - "value": "Select website" + "value": "Seleccionar sitio web" } ], "label.session": [ { "type": 0, - "value": "Session" + "value": "Sesión" } ], "label.sessions": [ { "type": 0, - "value": "Sessions" + "value": "Sesións" } ], "label.settings": [ @@ -1158,13 +1158,13 @@ "label.steps": [ { "type": 0, - "value": "Steps" + "value": "Pasos" } ], "label.sum": [ { "type": 0, - "value": "Sum" + "value": "Suma" } ], "label.tablet": [ @@ -1176,55 +1176,55 @@ "label.team": [ { "type": 0, - "value": "Team" + "value": "Equipo" } ], "label.team-id": [ { "type": 0, - "value": "Team ID" + "value": "ID do equipo" } ], "label.team-manager": [ { "type": 0, - "value": "Team manager" + "value": "Xestor do equipo" } ], "label.team-member": [ { "type": 0, - "value": "Team member" + "value": "Membro do equipo" } ], "label.team-name": [ { "type": 0, - "value": "Team name" + "value": "Nome do equipo" } ], "label.team-owner": [ { "type": 0, - "value": "Team owner" + "value": "Propietario do equipo" } ], "label.team-view-only": [ { "type": 0, - "value": "Team view only" + "value": "Equipo de só lectura" } ], "label.team-websites": [ { "type": 0, - "value": "Team websites" + "value": "Sitios web do equipo" } ], "label.teams": [ { "type": 0, - "value": "Teams" + "value": "Equipos" } ], "label.theme": [ @@ -1260,7 +1260,7 @@ "label.title": [ { "type": 0, - "value": "Title" + "value": "Título" } ], "label.today": [ @@ -1284,49 +1284,49 @@ "label.total-records": [ { "type": 0, - "value": "Total records" + "value": "Rexistros totais" } ], "label.tracking-code": [ { "type": 0, - "value": "Código de seguimento" + "value": "Código de seguemento" } ], "label.transactions": [ { "type": 0, - "value": "Transactions" + "value": "Transaccións" } ], "label.transfer": [ { "type": 0, - "value": "Transfer" + "value": "Transferir" } ], "label.transfer-website": [ { "type": 0, - "value": "Transfer website" + "value": "Transferir sitio web" } ], "label.true": [ { "type": 0, - "value": "True" + "value": "Verdadeiro" } ], "label.type": [ { "type": 0, - "value": "Type" + "value": "Tipo" } ], "label.unique": [ { "type": 0, - "value": "Unique" + "value": "Único" } ], "label.unique-visitors": [ @@ -1338,7 +1338,7 @@ "label.uniqueCustomers": [ { "type": 0, - "value": "Unique Customers" + "value": "Clientes únicos" } ], "label.unknown": [ @@ -1350,13 +1350,13 @@ "label.untitled": [ { "type": 0, - "value": "Untitled" + "value": "Sen título" } ], "label.update": [ { "type": 0, - "value": "Update" + "value": "Actualizar" } ], "label.url": [ @@ -1374,13 +1374,13 @@ "label.user": [ { "type": 0, - "value": "User" + "value": "Usuario" } ], "label.user-property": [ { "type": 0, - "value": "User Property" + "value": "Propiedade do usuario" } ], "label.username": [ @@ -1392,7 +1392,7 @@ "label.users": [ { "type": 0, - "value": "Users" + "value": "Usuarios" } ], "label.utm": [ @@ -1404,19 +1404,19 @@ "label.utm-description": [ { "type": 0, - "value": "Track your campaigns through UTM parameters." + "value": "Segue as túas campañas a través dos parámetros UTM." } ], "label.value": [ { "type": 0, - "value": "Value" + "value": "Valor" } ], "label.view": [ { "type": 0, - "value": "View" + "value": "Vista" } ], "label.view-details": [ @@ -1428,7 +1428,7 @@ "label.view-only": [ { "type": 0, - "value": "View only" + "value": "Só lectura" } ], "label.views": [ @@ -1440,7 +1440,7 @@ "label.views-per-visit": [ { "type": 0, - "value": "Views per visit" + "value": "Visualizacións por visita" } ], "label.visit-duration": [ @@ -1458,19 +1458,19 @@ "label.visits": [ { "type": 0, - "value": "Visits" + "value": "Visitas" } ], "label.website": [ { "type": 0, - "value": "Website" + "value": "Sitio web" } ], "label.website-id": [ { "type": 0, - "value": "Website ID" + "value": "ID do sitio web" } ], "label.websites": [ @@ -1482,19 +1482,19 @@ "label.window": [ { "type": 0, - "value": "Window" + "value": "Ventá" } ], "label.yesterday": [ { "type": 0, - "value": "Yesterday" + "value": "Onte" } ], "message.action-confirmation": [ { "type": 0, - "value": "Type " + "value": "Escribe " }, { "type": 1, @@ -1502,7 +1502,7 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " na caixa de embaixo para confirmar." } ], "message.active-users": [ @@ -1542,13 +1542,13 @@ "message.collected-data": [ { "type": 0, - "value": "Collected data" + "value": "Datos recopilados" } ], "message.confirm-delete": [ { "type": 0, - "value": "Tes a certeza de querer eliminar " + "value": "Estás seguro/a de que queres eliminar " }, { "type": 1, @@ -1562,7 +1562,7 @@ "message.confirm-leave": [ { "type": 0, - "value": "Are you sure you want to leave " + "value": "Estás seguro/a de que queres deixar " }, { "type": 1, @@ -1576,7 +1576,7 @@ "message.confirm-remove": [ { "type": 0, - "value": "Are you sure you want to remove " + "value": "Estás seguro/a de que queres eliminar " }, { "type": 1, @@ -1590,7 +1590,7 @@ "message.confirm-reset": [ { "type": 0, - "value": "Tes a certeza de querer restablecer as estatísticas de " + "value": "Estás seguro/a de querer restablecer as estatísticas de " }, { "type": 1, @@ -1604,7 +1604,7 @@ "message.delete-team-warning": [ { "type": 0, - "value": "Deleting a team will also delete all team websites." + "value": "Eliminar un equipo tamén eliminará tódolos sitios web do equipo." } ], "message.delete-website-warning": [ @@ -1626,7 +1626,7 @@ }, { "type": 0, - "value": " on " + "value": " en " }, { "type": 1, @@ -1654,7 +1654,7 @@ "message.min-password-length": [ { "type": 0, - "value": "Minimum length of " + "value": "Lonxitude mínima de " }, { "type": 1, @@ -1662,13 +1662,13 @@ }, { "type": 0, - "value": " characters" + "value": " caracteres" } ], "message.new-version-available": [ { "type": 0, - "value": "A new version of Umami " + "value": "Unha nova versión de Umami " }, { "type": 1, @@ -1676,7 +1676,7 @@ }, { "type": 0, - "value": " is available!" + "value": " está dispoñible!" } ], "message.no-data-available": [ @@ -1688,7 +1688,7 @@ "message.no-event-data": [ { "type": 0, - "value": "No event data is available." + "value": "Sen datos de eventos dispoñibles." } ], "message.no-match-password": [ @@ -1700,25 +1700,25 @@ "message.no-results-found": [ { "type": 0, - "value": "No results were found." + "value": "Non se atoparon resultados." } ], "message.no-team-websites": [ { "type": 0, - "value": "This team does not have any websites." + "value": "Este equipo non ten ningún sitio web." } ], "message.no-teams": [ { "type": 0, - "value": "You have not created any teams." + "value": "Non creaches ningún equipo." } ], "message.no-users": [ { "type": 0, - "value": "There are no users." + "value": "Non hai usuarios." } ], "message.no-websites-configured": [ @@ -1736,7 +1736,7 @@ "message.reset-website": [ { "type": 0, - "value": "To reset this website, type " + "value": "Para restablecer este sitio web, escriba " }, { "type": 1, @@ -1744,7 +1744,7 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " na caixa de embaixo para confirmar." } ], "message.reset-website-warning": [ @@ -1776,19 +1776,19 @@ "message.team-already-member": [ { "type": 0, - "value": "You are already a member of the team." + "value": "Xa es membro do equipo." } ], "message.team-not-found": [ { "type": 0, - "value": "Team not found." + "value": "Equipo non atopado." } ], "message.team-websites-info": [ { "type": 0, - "value": "Websites can be viewed by anyone on the team." + "value": "Os sitios web poden ser vistos por calquera membro do equipo." } ], "message.tracking-code": [ @@ -1800,37 +1800,37 @@ "message.transfer-team-website-to-user": [ { "type": 0, - "value": "Transfer this website to your account?" + "value": "Transferir este sitio web á túa conta?" } ], "message.transfer-user-website-to-team": [ { "type": 0, - "value": "Select the team to transfer this website to." + "value": "Selecciona o equipo ao que transferir este sitio web." } ], "message.transfer-website": [ { "type": 0, - "value": "Transfer website ownership to your account or another team." + "value": "Transferir propiedade do sitio web á túa conta ou a outro equipo." } ], "message.triggered-event": [ { "type": 0, - "value": "Triggered event" + "value": "Activou o evento" } ], "message.user-deleted": [ { "type": 0, - "value": "User deleted." + "value": "Usuario eliminado." } ], "message.viewed-page": [ { "type": 0, - "value": "Viewed page" + "value": "Páxina vista" } ], "message.visitor-log": [ @@ -1870,7 +1870,7 @@ "message.visitors-dropped-off": [ { "type": 0, - "value": "Visitors dropped off" + "value": "Visitantes abandonados" } ] } diff --git a/public/intl/messages/hi-IN.json b/public/intl/messages/hi-IN.json index fc133a93..097349a3 100644 --- a/public/intl/messages/hi-IN.json +++ b/public/intl/messages/hi-IN.json @@ -2,7 +2,7 @@ "label.access-code": [ { "type": 0, - "value": "Access code" + "value": "पहुंच कोड" } ], "label.actions": [ @@ -14,19 +14,19 @@ "label.activity": [ { "type": 0, - "value": "Activity log" + "value": "गतिविधि लॉग" } ], "label.add": [ { "type": 0, - "value": "Add" + "value": "जोडो" } ], "label.add-description": [ { "type": 0, - "value": "Add description" + "value": "विवरण लिखें" } ], "label.add-member": [ @@ -1006,7 +1006,7 @@ "label.reports": [ { "type": 0, - "value": "Reports" + "value": "प्रतिवेदन" } ], "label.required": [ diff --git a/rollup.components.config.mjs b/rollup.components.config.mjs index 9be07390..afeaf83c 100644 --- a/rollup.components.config.mjs +++ b/rollup.components.config.mjs @@ -19,13 +19,8 @@ const customResolver = resolve({ const aliasConfig = { entries: [ - { find: /^app/, replacement: path.resolve('./src/app') }, - { find: /^components/, replacement: path.resolve('./src/components') }, - { find: /^hooks/, replacement: path.resolve('./src/hooks') }, - { find: /^lib/, replacement: path.resolve('./src/lib') }, - { find: /^store/, replacement: path.resolve('./src/store') }, + { find: /^@/, replacement: path.resolve('./src/') }, { find: /^public/, replacement: path.resolve('./public') }, - { find: /^assets/, replacement: path.resolve('./src/assets') }, ], customResolver, }; diff --git a/scripts/change-password.js b/scripts/change-password.js deleted file mode 100644 index b12373a9..00000000 --- a/scripts/change-password.js +++ /dev/null @@ -1,94 +0,0 @@ -/* eslint-disable no-console */ -require('dotenv').config(); -const { hashPassword } = require('next-basics'); -const chalk = require('chalk'); -const prompts = require('prompts'); -const { PrismaClient } = require('@prisma/client'); - -const prisma = new PrismaClient(); - -const runQuery = async query => { - return query.catch(e => { - throw e; - }); -}; - -const updateUserByUsername = (username, data) => { - return runQuery( - prisma.user.update({ - where: { - username, - }, - data, - }), - ); -}; - -const changePassword = async (username, newPassword) => { - const password = hashPassword(newPassword); - return updateUserByUsername(username, { password }); -}; - -const getUsernameAndPassword = async () => { - let [username, password] = process.argv.slice(2); - if (username && password) { - return { username, password }; - } - - const questions = []; - if (!username) { - questions.push({ - type: 'text', - name: 'username', - message: 'Enter user to change password', - }); - } - if (!password) { - questions.push( - { - type: 'password', - name: 'password', - message: 'Enter new password', - }, - { - type: 'password', - name: 'confirmation', - message: 'Confirm new password', - }, - ); - } - - const answers = await prompts(questions); - if (answers.password !== answers.confirmation) { - throw new Error(`Passwords don't match`); - } - - return { - username: username || answers.username, - password: answers.password, - }; -}; - -(async () => { - let username, password; - - try { - ({ username, password } = await getUsernameAndPassword()); - } catch (error) { - console.log(chalk.redBright(error.message)); - return; - } - - try { - await changePassword(username, password); - console.log('Password changed for user', chalk.greenBright(username)); - } catch (error) { - if (error.meta.cause.includes('Record to update not found')) { - console.log('User not found:', chalk.redBright(username)); - } else { - throw error; - } - } - - prisma.$disconnect(); -})(); diff --git a/scripts/check-env.js b/scripts/check-env.js index 280e7e39..e8c80a5d 100644 --- a/scripts/check-env.js +++ b/scripts/check-env.js @@ -23,5 +23,5 @@ if (!process.env.SKIP_DB_CHECK && !process.env.DATABASE_TYPE) { } if (process.env.CLOUD_MODE) { - checkMissing(['CLOUD_URL', 'KAFKA_BROKER', 'KAFKA_URL', 'REDIS_URL']); + checkMissing(['CLOUD_URL', 'KAFKA_BROKER', 'KAFKA_URL', 'REDIS_URL', 'KAFKA_SASL_MECHANISM']); } diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx index efb38043..4cbb1c80 100644 --- a/src/app/(main)/App.tsx +++ b/src/app/(main)/App.tsx @@ -2,7 +2,7 @@ import { Loading } from 'react-basics'; import Script from 'next/script'; import { usePathname } from 'next/navigation'; -import { useLogin, useConfig } from 'components/hooks'; +import { useLogin, useConfig } from '@/components/hooks'; import UpdateNotice from './UpdateNotice'; export function App({ children }) { @@ -22,6 +22,10 @@ export function App({ children }) { return null; } + if (config.uiDisabled) { + return null; + } + return ( <> {children} diff --git a/src/app/(main)/NavBar.tsx b/src/app/(main)/NavBar.tsx index 5c8bba01..147f7085 100644 --- a/src/app/(main)/NavBar.tsx +++ b/src/app/(main)/NavBar.tsx @@ -1,17 +1,17 @@ 'use client'; +import { useEffect } from 'react'; import { Icon, Text } from 'react-basics'; import Link from 'next/link'; import classNames from 'classnames'; -import HamburgerButton from 'components/common/HamburgerButton'; -import ThemeButton from 'components/input/ThemeButton'; -import LanguageButton from 'components/input/LanguageButton'; -import ProfileButton from 'components/input/ProfileButton'; -import TeamsButton from 'components/input/TeamsButton'; -import Icons from 'components/icons'; -import { useMessages, useNavigation, useTeamUrl } from 'components/hooks'; +import HamburgerButton from '@/components/common/HamburgerButton'; +import ThemeButton from '@/components/input/ThemeButton'; +import LanguageButton from '@/components/input/LanguageButton'; +import ProfileButton from '@/components/input/ProfileButton'; +import TeamsButton from '@/components/input/TeamsButton'; +import Icons from '@/components/icons'; +import { useMessages, useNavigation, useTeamUrl } from '@/components/hooks'; +import { getItem, setItem } from '@/lib/storage'; import styles from './NavBar.module.css'; -import { useEffect } from 'react'; -import { getItem, setItem } from 'next-basics'; export function NavBar() { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/UpdateNotice.tsx b/src/app/(main)/UpdateNotice.tsx index 553e1138..17c2bce7 100644 --- a/src/app/(main)/UpdateNotice.tsx +++ b/src/app/(main)/UpdateNotice.tsx @@ -1,10 +1,10 @@ import { useEffect, useCallback, useState } from 'react'; import { createPortal } from 'react-dom'; import { Button } from 'react-basics'; -import { setItem } from 'next-basics'; -import useStore, { checkVersion } from 'store/version'; -import { REPO_URL, VERSION_CHECK } from 'lib/constants'; -import { useMessages } from 'components/hooks'; +import { setItem } from '@/lib/storage'; +import useStore, { checkVersion } from '@/store/version'; +import { REPO_URL, VERSION_CHECK } from '@/lib/constants'; +import { useMessages } from '@/components/hooks'; import { usePathname } from 'next/navigation'; import styles from './UpdateNotice.module.css'; @@ -14,6 +14,7 @@ export function UpdateNotice({ user, config }) { const pathname = usePathname(); const [dismissed, setDismissed] = useState(checked); const allowUpdate = + process.env.NODE_ENV === 'production' && user?.isAdmin && !config?.updatesDisabled && !pathname.includes('/share/') && diff --git a/src/app/(main)/console/TestConsole.tsx b/src/app/(main)/console/TestConsole.tsx index 6842b26d..21b98df6 100644 --- a/src/app/(main)/console/TestConsole.tsx +++ b/src/app/(main)/console/TestConsole.tsx @@ -1,12 +1,12 @@ import { Button } from 'react-basics'; import Link from 'next/link'; import Script from 'next/script'; -import WebsiteSelect from 'components/input/WebsiteSelect'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import EventsChart from 'components/metrics/EventsChart'; +import WebsiteSelect from '@/components/input/WebsiteSelect'; +import Page from '@/components/layout/Page'; +import PageHeader from '@/components/layout/PageHeader'; +import EventsChart from '@/components/metrics/EventsChart'; import WebsiteChart from '../websites/[websiteId]/WebsiteChart'; -import { useApi, useNavigation } from 'components/hooks'; +import { useApi, useNavigation } from '@/components/hooks'; import styles from './TestConsole.module.css'; export function TestConsole({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/dashboard/DashboardEdit.tsx b/src/app/(main)/dashboard/DashboardEdit.tsx index 42eb99ac..d15ae197 100644 --- a/src/app/(main)/dashboard/DashboardEdit.tsx +++ b/src/app/(main)/dashboard/DashboardEdit.tsx @@ -1,10 +1,10 @@ import { useState, useMemo, useEffect } from 'react'; -import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; +import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import classNames from 'classnames'; import { Button, Loading, Toggle, SearchField } from 'react-basics'; import { firstBy } from 'thenby'; -import useDashboard, { saveDashboard } from 'store/dashboard'; -import { useMessages, useWebsites } from 'components/hooks'; +import useDashboard, { saveDashboard } from '@/store/dashboard'; +import { useMessages, useWebsites } from '@/components/hooks'; import styles from './DashboardEdit.module.css'; const DRAG_ID = 'dashboard-website-ordering'; diff --git a/src/app/(main)/dashboard/DashboardPage.tsx b/src/app/(main)/dashboard/DashboardPage.tsx index 2a4bb916..83b27e09 100644 --- a/src/app/(main)/dashboard/DashboardPage.tsx +++ b/src/app/(main)/dashboard/DashboardPage.tsx @@ -1,14 +1,14 @@ 'use client'; import { Icon, Icons, Loading, Text } from 'react-basics'; -import PageHeader from 'components/layout/PageHeader'; -import Pager from 'components/common/Pager'; +import PageHeader from '@/components/layout/PageHeader'; +import Pager from '@/components/common/Pager'; import WebsiteChartList from '../websites/[websiteId]/WebsiteChartList'; -import DashboardSettingsButton from 'app/(main)/dashboard/DashboardSettingsButton'; -import DashboardEdit from 'app/(main)/dashboard/DashboardEdit'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import { useMessages, useLocale, useTeamUrl, useWebsites } from 'components/hooks'; -import useDashboard from 'store/dashboard'; -import LinkButton from 'components/common/LinkButton'; +import DashboardSettingsButton from '@/app/(main)/dashboard/DashboardSettingsButton'; +import DashboardEdit from '@/app/(main)/dashboard/DashboardEdit'; +import EmptyPlaceholder from '@/components/common/EmptyPlaceholder'; +import { useMessages, useLocale, useTeamUrl, useWebsites } from '@/components/hooks'; +import useDashboard from '@/store/dashboard'; +import LinkButton from '@/components/common/LinkButton'; export function DashboardPage() { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/dashboard/DashboardSettingsButton.tsx b/src/app/(main)/dashboard/DashboardSettingsButton.tsx index 9e1d3dbc..1c473a22 100644 --- a/src/app/(main)/dashboard/DashboardSettingsButton.tsx +++ b/src/app/(main)/dashboard/DashboardSettingsButton.tsx @@ -1,7 +1,7 @@ import { TooltipPopup, Icon, Text, Flexbox, Button } from 'react-basics'; -import Icons from 'components/icons'; -import { saveDashboard } from 'store/dashboard'; -import { useMessages } from 'components/hooks'; +import Icons from '@/components/icons'; +import { saveDashboard } from '@/store/dashboard'; +import { useMessages } from '@/components/hooks'; export function DashboardSettingsButton() { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index ba221990..dd1baec8 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -1,14 +1,10 @@ import { Metadata } from 'next'; import App from './App'; import NavBar from './NavBar'; -import Page from 'components/layout/Page'; +import Page from '@/components/layout/Page'; import styles from './layout.module.css'; -export default function ({ children }) { - if (process.env.DISABLE_UI) { - return null; - } - +export default async function ({ children }) { return (
diff --git a/src/app/(main)/profile/DateRangeSetting.tsx b/src/app/(main)/profile/DateRangeSetting.tsx index 25b5afbd..37d2ca43 100644 --- a/src/app/(main)/profile/DateRangeSetting.tsx +++ b/src/app/(main)/profile/DateRangeSetting.tsx @@ -1,8 +1,8 @@ -import DateFilter from 'components/input/DateFilter'; +import DateFilter from '@/components/input/DateFilter'; import { Button, Flexbox } from 'react-basics'; -import { useDateRange, useMessages } from 'components/hooks'; -import { DEFAULT_DATE_RANGE } from 'lib/constants'; -import { DateRange } from 'lib/types'; +import { useDateRange, useMessages } from '@/components/hooks'; +import { DEFAULT_DATE_RANGE } from '@/lib/constants'; +import { DateRange } from '@/lib/types'; import styles from './DateRangeSetting.module.css'; export function DateRangeSetting() { diff --git a/src/app/(main)/profile/LanguageSetting.tsx b/src/app/(main)/profile/LanguageSetting.tsx index 41ff3dde..a47394b3 100644 --- a/src/app/(main)/profile/LanguageSetting.tsx +++ b/src/app/(main)/profile/LanguageSetting.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; import { Button, Dropdown, Item, Flexbox } from 'react-basics'; -import { useLocale, useMessages } from 'components/hooks'; -import { DEFAULT_LOCALE } from 'lib/constants'; -import { languages } from 'lib/lang'; +import { useLocale, useMessages } from '@/components/hooks'; +import { DEFAULT_LOCALE } from '@/lib/constants'; +import { languages } from '@/lib/lang'; import styles from './LanguageSetting.module.css'; export function LanguageSetting() { diff --git a/src/app/(main)/profile/PasswordChangeButton.tsx b/src/app/(main)/profile/PasswordChangeButton.tsx index 552663ca..63249a2b 100644 --- a/src/app/(main)/profile/PasswordChangeButton.tsx +++ b/src/app/(main)/profile/PasswordChangeButton.tsx @@ -1,7 +1,7 @@ import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics'; -import PasswordEditForm from 'app/(main)/profile/PasswordEditForm'; -import Icons from 'components/icons'; -import { useMessages } from 'components/hooks'; +import PasswordEditForm from '@/app/(main)/profile/PasswordEditForm'; +import Icons from '@/components/icons'; +import { useMessages } from '@/components/hooks'; export function PasswordChangeButton() { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/profile/PasswordEditForm.tsx b/src/app/(main)/profile/PasswordEditForm.tsx index 1402efa2..c352d516 100644 --- a/src/app/(main)/profile/PasswordEditForm.tsx +++ b/src/app/(main)/profile/PasswordEditForm.tsx @@ -1,6 +1,6 @@ import { useRef } from 'react'; import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics'; -import { useApi, useMessages } from 'components/hooks'; +import { useApi, useMessages } from '@/components/hooks'; export function PasswordEditForm({ onSave, onClose }) { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/profile/ProfileHeader.tsx b/src/app/(main)/profile/ProfileHeader.tsx index e2d69cc7..05871fba 100644 --- a/src/app/(main)/profile/ProfileHeader.tsx +++ b/src/app/(main)/profile/ProfileHeader.tsx @@ -1,5 +1,5 @@ -import PageHeader from 'components/layout/PageHeader'; -import { useMessages } from 'components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; +import { useMessages } from '@/components/hooks'; export function ProfileHeader() { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/profile/ProfileSettings.tsx b/src/app/(main)/profile/ProfileSettings.tsx index 9c7db39f..f9dfe06d 100644 --- a/src/app/(main)/profile/ProfileSettings.tsx +++ b/src/app/(main)/profile/ProfileSettings.tsx @@ -1,11 +1,11 @@ import { Form, FormRow } from 'react-basics'; -import TimezoneSetting from 'app/(main)/profile/TimezoneSetting'; -import DateRangeSetting from 'app/(main)/profile/DateRangeSetting'; -import LanguageSetting from 'app/(main)/profile/LanguageSetting'; -import ThemeSetting from 'app/(main)/profile/ThemeSetting'; +import TimezoneSetting from '@/app/(main)/profile/TimezoneSetting'; +import DateRangeSetting from '@/app/(main)/profile/DateRangeSetting'; +import LanguageSetting from '@/app/(main)/profile/LanguageSetting'; +import ThemeSetting from '@/app/(main)/profile/ThemeSetting'; import PasswordChangeButton from './PasswordChangeButton'; -import { useLogin, useMessages } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import { useLogin, useMessages } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; export function ProfileSettings() { const { user } = useLogin(); diff --git a/src/app/(main)/profile/ThemeSetting.tsx b/src/app/(main)/profile/ThemeSetting.tsx index 577728b7..49ea7161 100644 --- a/src/app/(main)/profile/ThemeSetting.tsx +++ b/src/app/(main)/profile/ThemeSetting.tsx @@ -1,8 +1,8 @@ import classNames from 'classnames'; import { Button, Icon } from 'react-basics'; -import { useTheme } from 'components/hooks'; -import Sun from 'assets/sun.svg'; -import Moon from 'assets/moon.svg'; +import { useTheme } from '@/components/hooks'; +import Sun from '@/assets/sun.svg'; +import Moon from '@/assets/moon.svg'; import styles from './ThemeSetting.module.css'; export function ThemeSetting() { diff --git a/src/app/(main)/profile/TimezoneSetting.tsx b/src/app/(main)/profile/TimezoneSetting.tsx index 00858ac7..56c85813 100644 --- a/src/app/(main)/profile/TimezoneSetting.tsx +++ b/src/app/(main)/profile/TimezoneSetting.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Dropdown, Item, Button, Flexbox } from 'react-basics'; -import { useTimezone, useMessages } from 'components/hooks'; -import { getTimezone } from 'lib/date'; +import { useTimezone, useMessages } from '@/components/hooks'; +import { getTimezone } from '@/lib/date'; import styles from './TimezoneSetting.module.css'; const timezones = Intl.supportedValuesOf('timeZone'); diff --git a/src/app/(main)/reports/ReportDeleteButton.tsx b/src/app/(main)/reports/ReportDeleteButton.tsx index d51f7144..efd1da3c 100644 --- a/src/app/(main)/reports/ReportDeleteButton.tsx +++ b/src/app/(main)/reports/ReportDeleteButton.tsx @@ -1,6 +1,6 @@ import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import { useApi, useMessages, useModified } from 'components/hooks'; -import ConfirmationForm from 'components/common/ConfirmationForm'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import ConfirmationForm from '@/components/common/ConfirmationForm'; export function ReportDeleteButton({ reportId, @@ -11,7 +11,7 @@ export function ReportDeleteButton({ reportName: string; onDelete?: () => void; }) { - const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { formatMessage, labels, messages } = useMessages(); const { del, useMutation } = useApi(); const { mutate, isPending, error } = useMutation({ mutationFn: reportId => del(`/reports/${reportId}`), @@ -39,12 +39,7 @@ export function ReportDeleteButton({ {(close: () => void) => ( {reportName} }} - /> - } + message={formatMessage(messages.confirmDelete, { target: {reportName} })} isLoading={isPending} error={error} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/app/(main)/reports/ReportsDataTable.tsx b/src/app/(main)/reports/ReportsDataTable.tsx index b03edfc2..0cc5a96c 100644 --- a/src/app/(main)/reports/ReportsDataTable.tsx +++ b/src/app/(main)/reports/ReportsDataTable.tsx @@ -1,6 +1,6 @@ -import { useReports } from 'components/hooks'; +import { useReports } from '@/components/hooks'; import ReportsTable from './ReportsTable'; -import DataTable from 'components/common/DataTable'; +import DataTable from '@/components/common/DataTable'; import { ReactNode } from 'react'; export default function ReportsDataTable({ diff --git a/src/app/(main)/reports/ReportsHeader.tsx b/src/app/(main)/reports/ReportsHeader.tsx index 92f538ea..ff9cb294 100644 --- a/src/app/(main)/reports/ReportsHeader.tsx +++ b/src/app/(main)/reports/ReportsHeader.tsx @@ -1,8 +1,8 @@ -import PageHeader from 'components/layout/PageHeader'; +import PageHeader from '@/components/layout/PageHeader'; import { Icon, Icons, Text } from 'react-basics'; -import { useLogin, useMessages, useTeamUrl } from 'components/hooks'; -import LinkButton from 'components/common/LinkButton'; -import { ROLES } from 'lib/constants'; +import { useLogin, useMessages, useTeamUrl } from '@/components/hooks'; +import LinkButton from '@/components/common/LinkButton'; +import { ROLES } from '@/lib/constants'; export function ReportsHeader() { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/reports/ReportsPage.tsx b/src/app/(main)/reports/ReportsPage.tsx index 5b031cfa..64d43c70 100644 --- a/src/app/(main)/reports/ReportsPage.tsx +++ b/src/app/(main)/reports/ReportsPage.tsx @@ -2,7 +2,7 @@ import { Metadata } from 'next'; import ReportsHeader from './ReportsHeader'; import ReportsDataTable from './ReportsDataTable'; -import { useTeamUrl } from 'components/hooks'; +import { useTeamUrl } from '@/components/hooks'; export default function ReportsPage() { const { teamId } = useTeamUrl(); diff --git a/src/app/(main)/reports/ReportsTable.tsx b/src/app/(main)/reports/ReportsTable.tsx index c35468aa..a891b6d0 100644 --- a/src/app/(main)/reports/ReportsTable.tsx +++ b/src/app/(main)/reports/ReportsTable.tsx @@ -1,7 +1,7 @@ import { GridColumn, GridTable, Icon, Icons, Text } from 'react-basics'; -import LinkButton from 'components/common/LinkButton'; -import { useMessages, useLogin, useTeamUrl } from 'components/hooks'; -import { REPORT_TYPES } from 'lib/constants'; +import LinkButton from '@/components/common/LinkButton'; +import { useMessages, useLogin, useTeamUrl } from '@/components/hooks'; +import { REPORT_TYPES } from '@/lib/constants'; import ReportDeleteButton from './ReportDeleteButton'; export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) { diff --git a/src/app/(main)/reports/[reportId]/BaseParameters.tsx b/src/app/(main)/reports/[reportId]/BaseParameters.tsx index 77de51f3..1f4881be 100644 --- a/src/app/(main)/reports/[reportId]/BaseParameters.tsx +++ b/src/app/(main)/reports/[reportId]/BaseParameters.tsx @@ -1,9 +1,9 @@ import { useContext } from 'react'; import { FormRow } from 'react-basics'; -import { parseDateRange } from 'lib/date'; -import DateFilter from 'components/input/DateFilter'; -import WebsiteSelect from 'components/input/WebsiteSelect'; -import { useMessages, useTeamUrl, useWebsite } from 'components/hooks'; +import { parseDateRange } from '@/lib/date'; +import DateFilter from '@/components/input/DateFilter'; +import WebsiteSelect from '@/components/input/WebsiteSelect'; +import { useMessages, useTeamUrl, useWebsite } from '@/components/hooks'; import { ReportContext } from './Report'; import styles from './BaseParameters.module.css'; diff --git a/src/app/(main)/reports/[reportId]/FieldAddForm.tsx b/src/app/(main)/reports/[reportId]/FieldAddForm.tsx index 9217ce4d..6560a947 100644 --- a/src/app/(main)/reports/[reportId]/FieldAddForm.tsx +++ b/src/app/(main)/reports/[reportId]/FieldAddForm.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { createPortal } from 'react-dom'; -import { REPORT_PARAMETERS } from 'lib/constants'; +import { REPORT_PARAMETERS } from '@/lib/constants'; import PopupForm from './PopupForm'; import FieldSelectForm from './FieldSelectForm'; diff --git a/src/app/(main)/reports/[reportId]/FieldAggregateForm.tsx b/src/app/(main)/reports/[reportId]/FieldAggregateForm.tsx index 6b5bf636..5db0e580 100644 --- a/src/app/(main)/reports/[reportId]/FieldAggregateForm.tsx +++ b/src/app/(main)/reports/[reportId]/FieldAggregateForm.tsx @@ -1,5 +1,5 @@ import { Form, FormRow, Menu, Item } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export default function FieldAggregateForm({ name, diff --git a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx index a1417780..c1f95e80 100644 --- a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx +++ b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx @@ -1,7 +1,7 @@ -import { useFilters, useFormat, useLocale, useMessages, useWebsiteValues } from 'components/hooks'; -import { OPERATORS } from 'lib/constants'; -import { isEqualsOperator } from 'lib/params'; import { useMemo, useState } from 'react'; +import { useFilters, useFormat, useMessages, useWebsiteValues } from '@/components/hooks'; +import { OPERATORS } from '@/lib/constants'; +import { isEqualsOperator } from '@/lib/params'; import { Button, Dropdown, @@ -55,7 +55,6 @@ export default function FieldFilterEditForm({ const [selected, setSelected] = useState(isEquals ? value : ''); const { filters } = useFilters(); const { formatValue } = useFormat(); - const { locale } = useLocale(); const isDisabled = !operator || (isEquals && !selected) || (!isEquals && !value); const { data: values = [], @@ -80,29 +79,17 @@ export default function FieldFilterEditForm({ }; const formattedValues = useMemo(() => { - if (!values) { - return {}; - } - const formatted = {}; - const format = (val: string) => { - formatted[val] = formatValue(val, name); - return formatted[val]; - }; + return values.reduce((obj: { [x: string]: string }, { value }: { value: string }) => { + obj[value] = formatValue(value, name); - if (values?.length !== 1) { - const { compare } = new Intl.Collator(locale, { numeric: true }); - values.sort((a, b) => compare(formatted[a] ?? format(a), formatted[b] ?? format(b))); - } else { - format(values[0]); - } - - return formatted; - }, [formatValue, locale, name, values]); + return obj; + }, {}); + }, [formatValue, name, values]); const filteredValues = useMemo(() => { return value ? values.filter((n: string | number) => - formattedValues[n].toLowerCase().includes(value.toLowerCase()), + formattedValues[n]?.toLowerCase()?.includes(value.toLowerCase()), ) : values; }, [value, formattedValues]); @@ -226,7 +213,7 @@ const ResultsMenu = ({ values, type, isLoading, onSelect }) => { return ( - {values?.map((value: any) => { + {values?.map(({ value }) => { return {formatValue(value, type)}; })} diff --git a/src/app/(main)/reports/[reportId]/FieldParameters.tsx b/src/app/(main)/reports/[reportId]/FieldParameters.tsx index 36cfbda9..de80cc69 100644 --- a/src/app/(main)/reports/[reportId]/FieldParameters.tsx +++ b/src/app/(main)/reports/[reportId]/FieldParameters.tsx @@ -1,5 +1,5 @@ -import { useFields, useMessages } from 'components/hooks'; -import Icons from 'components/icons'; +import { useFields, useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; import { useContext } from 'react'; import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics'; import FieldSelectForm from '../[reportId]/FieldSelectForm'; diff --git a/src/app/(main)/reports/[reportId]/FieldSelectForm.tsx b/src/app/(main)/reports/[reportId]/FieldSelectForm.tsx index dfd402cf..f73d59f7 100644 --- a/src/app/(main)/reports/[reportId]/FieldSelectForm.tsx +++ b/src/app/(main)/reports/[reportId]/FieldSelectForm.tsx @@ -1,5 +1,5 @@ import { Menu, Item, Form, FormRow } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './FieldSelectForm.module.css'; import { Key } from 'react'; diff --git a/src/app/(main)/reports/[reportId]/FilterParameters.tsx b/src/app/(main)/reports/[reportId]/FilterParameters.tsx index ddbe4d1e..538c4ce5 100644 --- a/src/app/(main)/reports/[reportId]/FilterParameters.tsx +++ b/src/app/(main)/reports/[reportId]/FilterParameters.tsx @@ -1,13 +1,13 @@ import { useContext } from 'react'; -import { useMessages, useFormat, useFilters, useFields } from 'components/hooks'; -import Icons from 'components/icons'; +import { useMessages, useFormat, useFilters, useFields } from '@/components/hooks'; +import Icons from '@/components/icons'; import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics'; import FilterSelectForm from '../[reportId]/FilterSelectForm'; import ParameterList from '../[reportId]/ParameterList'; import PopupForm from '../[reportId]/PopupForm'; import { ReportContext } from './Report'; import FieldFilterEditForm from '../[reportId]/FieldFilterEditForm'; -import { isSearchOperator } from 'lib/params'; +import { isSearchOperator } from '@/lib/params'; import styles from './FilterParameters.module.css'; export function FilterParameters() { diff --git a/src/app/(main)/reports/[reportId]/ParameterList.tsx b/src/app/(main)/reports/[reportId]/ParameterList.tsx index d4b6477b..3c0401a0 100644 --- a/src/app/(main)/reports/[reportId]/ParameterList.tsx +++ b/src/app/(main)/reports/[reportId]/ParameterList.tsx @@ -1,8 +1,8 @@ import { ReactNode } from 'react'; import { Icon } from 'react-basics'; -import Icons from 'components/icons'; -import Empty from 'components/common/Empty'; -import { useMessages } from 'components/hooks'; +import Icons from '@/components/icons'; +import Empty from '@/components/common/Empty'; +import { useMessages } from '@/components/hooks'; import styles from './ParameterList.module.css'; import classNames from 'classnames'; diff --git a/src/app/(main)/reports/[reportId]/Report.tsx b/src/app/(main)/reports/[reportId]/Report.tsx index d6de9d42..1aed007c 100644 --- a/src/app/(main)/reports/[reportId]/Report.tsx +++ b/src/app/(main)/reports/[reportId]/Report.tsx @@ -1,7 +1,7 @@ import { createContext, ReactNode } from 'react'; import { Loading } from 'react-basics'; import classNames from 'classnames'; -import { useReport } from 'components/hooks'; +import { useReport } from '@/components/hooks'; import styles from './Report.module.css'; export const ReportContext = createContext(null); diff --git a/src/app/(main)/reports/[reportId]/ReportHeader.tsx b/src/app/(main)/reports/[reportId]/ReportHeader.tsx index 7ab80fcd..816a2df3 100644 --- a/src/app/(main)/reports/[reportId]/ReportHeader.tsx +++ b/src/app/(main)/reports/[reportId]/ReportHeader.tsx @@ -1,10 +1,10 @@ import { useContext } from 'react'; import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics'; -import { useMessages, useApi, useNavigation, useTeamUrl } from 'components/hooks'; +import { useMessages, useApi, useNavigation, useTeamUrl } from '@/components/hooks'; import { ReportContext } from './Report'; import styles from './ReportHeader.module.css'; -import { REPORT_TYPES } from 'lib/constants'; -import Breadcrumb from 'components/common/Breadcrumb'; +import { REPORT_TYPES } from '@/lib/constants'; +import Breadcrumb from '@/components/common/Breadcrumb'; export function ReportHeader({ icon }) { const { report, updateReport } = useContext(ReportContext); diff --git a/src/app/(main)/reports/[reportId]/ReportPage.tsx b/src/app/(main)/reports/[reportId]/ReportPage.tsx index da1a0342..8a3a94ad 100644 --- a/src/app/(main)/reports/[reportId]/ReportPage.tsx +++ b/src/app/(main)/reports/[reportId]/ReportPage.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useReport } from 'components/hooks'; +import { useReport } from '@/components/hooks'; import EventDataReport from '../event-data/EventDataReport'; import FunnelReport from '../funnel/FunnelReport'; import GoalReport from '../goals/GoalsReport'; diff --git a/src/app/(main)/reports/create/ReportTemplates.tsx b/src/app/(main)/reports/create/ReportTemplates.tsx index af19d4aa..c26e3a91 100644 --- a/src/app/(main)/reports/create/ReportTemplates.tsx +++ b/src/app/(main)/reports/create/ReportTemplates.tsx @@ -1,12 +1,12 @@ -import Funnel from 'assets/funnel.svg'; -import Money from 'assets/money.svg'; -import Lightbulb from 'assets/lightbulb.svg'; -import Magnet from 'assets/magnet.svg'; -import Path from 'assets/path.svg'; -import Tag from 'assets/tag.svg'; -import Target from 'assets/target.svg'; -import { useMessages, useTeamUrl } from 'components/hooks'; -import PageHeader from 'components/layout/PageHeader'; +import Funnel from '@/assets/funnel.svg'; +import Money from '@/assets/money.svg'; +import Lightbulb from '@/assets/lightbulb.svg'; +import Magnet from '@/assets/magnet.svg'; +import Path from '@/assets/path.svg'; +import Tag from '@/assets/tag.svg'; +import Target from '@/assets/target.svg'; +import { useMessages, useTeamUrl } from '@/components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; import Link from 'next/link'; import { Button, Icon, Icons, Text } from 'react-basics'; import styles from './ReportTemplates.module.css'; diff --git a/src/app/(main)/reports/event-data/EventDataParameters.tsx b/src/app/(main)/reports/event-data/EventDataParameters.tsx index 7b61c112..9e931cf5 100644 --- a/src/app/(main)/reports/event-data/EventDataParameters.tsx +++ b/src/app/(main)/reports/event-data/EventDataParameters.tsx @@ -1,9 +1,9 @@ import { useContext } from 'react'; import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics'; -import Empty from 'components/common/Empty'; -import Icons from 'components/icons'; -import { useApi, useMessages } from 'components/hooks'; -import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants'; +import Empty from '@/components/common/Empty'; +import Icons from '@/components/icons'; +import { useApi, useMessages } from '@/components/hooks'; +import { DATA_TYPES, REPORT_PARAMETERS } from '@/lib/constants'; import { ReportContext } from '../[reportId]/Report'; import FieldAddForm from '../[reportId]/FieldAddForm'; import ParameterList from '../[reportId]/ParameterList'; diff --git a/src/app/(main)/reports/event-data/EventDataReport.tsx b/src/app/(main)/reports/event-data/EventDataReport.tsx index fb786b31..8205a488 100644 --- a/src/app/(main)/reports/event-data/EventDataReport.tsx +++ b/src/app/(main)/reports/event-data/EventDataReport.tsx @@ -4,7 +4,7 @@ import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; import EventDataParameters from './EventDataParameters'; import EventDataTable from './EventDataTable'; -import Nodes from 'assets/nodes.svg'; +import Nodes from '@/assets/nodes.svg'; const defaultParameters = { type: 'event-data', diff --git a/src/app/(main)/reports/event-data/EventDataTable.tsx b/src/app/(main)/reports/event-data/EventDataTable.tsx index 740cbce4..f42e792d 100644 --- a/src/app/(main)/reports/event-data/EventDataTable.tsx +++ b/src/app/(main)/reports/event-data/EventDataTable.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; import { GridTable, GridColumn } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { ReportContext } from '../[reportId]/Report'; export function EventDataTable() { diff --git a/src/app/(main)/reports/funnel/FunnelChart.tsx b/src/app/(main)/reports/funnel/FunnelChart.tsx index 0da71d6f..be3da614 100644 --- a/src/app/(main)/reports/funnel/FunnelChart.tsx +++ b/src/app/(main)/reports/funnel/FunnelChart.tsx @@ -1,8 +1,8 @@ import { useContext } from 'react'; import classNames from 'classnames'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { ReportContext } from '../[reportId]/Report'; -import { formatLongNumber } from 'lib/format'; +import { formatLongNumber } from '@/lib/format'; import styles from './FunnelChart.module.css'; export interface FunnelChartProps { diff --git a/src/app/(main)/reports/funnel/FunnelParameters.tsx b/src/app/(main)/reports/funnel/FunnelParameters.tsx index ef4ffbfb..3db57135 100644 --- a/src/app/(main)/reports/funnel/FunnelParameters.tsx +++ b/src/app/(main)/reports/funnel/FunnelParameters.tsx @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { Icon, Form, @@ -12,7 +12,7 @@ import { TextField, Button, } from 'react-basics'; -import Icons from 'components/icons'; +import Icons from '@/components/icons'; import FunnelStepAddForm from './FunnelStepAddForm'; import { ReportContext } from '../[reportId]/Report'; import BaseParameters from '../[reportId]/BaseParameters'; diff --git a/src/app/(main)/reports/funnel/FunnelReport.tsx b/src/app/(main)/reports/funnel/FunnelReport.tsx index 850bbd90..e0c90e4a 100644 --- a/src/app/(main)/reports/funnel/FunnelReport.tsx +++ b/src/app/(main)/reports/funnel/FunnelReport.tsx @@ -4,8 +4,8 @@ import Report from '../[reportId]/Report'; import ReportHeader from '../[reportId]/ReportHeader'; import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; -import Funnel from 'assets/funnel.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Funnel from '@/assets/funnel.svg'; +import { REPORT_TYPES } from '@/lib/constants'; const defaultParameters = { type: REPORT_TYPES.funnel, diff --git a/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx b/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx index 7d77b0c7..d7917d7d 100644 --- a/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx +++ b/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics'; import styles from './FunnelStepAddForm.module.css'; diff --git a/src/app/(main)/reports/goals/GoalsAddForm.tsx b/src/app/(main)/reports/goals/GoalsAddForm.tsx index a82eea28..b7354533 100644 --- a/src/app/(main)/reports/goals/GoalsAddForm.tsx +++ b/src/app/(main)/reports/goals/GoalsAddForm.tsx @@ -1,4 +1,4 @@ -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { useState } from 'react'; import { Button, Dropdown, Flexbox, FormRow, Item, TextField } from 'react-basics'; import styles from './GoalsAddForm.module.css'; diff --git a/src/app/(main)/reports/goals/GoalsChart.tsx b/src/app/(main)/reports/goals/GoalsChart.tsx index dedbd07c..34ea485e 100644 --- a/src/app/(main)/reports/goals/GoalsChart.tsx +++ b/src/app/(main)/reports/goals/GoalsChart.tsx @@ -1,8 +1,8 @@ import { useContext } from 'react'; import classNames from 'classnames'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { ReportContext } from '../[reportId]/Report'; -import { formatLongNumber } from 'lib/format'; +import { formatLongNumber } from '@/lib/format'; import styles from './GoalsChart.module.css'; export function GoalsChart({ className }: { className?: string; isLoading?: boolean }) { diff --git a/src/app/(main)/reports/goals/GoalsParameters.tsx b/src/app/(main)/reports/goals/GoalsParameters.tsx index 8d85dc20..51866645 100644 --- a/src/app/(main)/reports/goals/GoalsParameters.tsx +++ b/src/app/(main)/reports/goals/GoalsParameters.tsx @@ -1,6 +1,6 @@ -import { useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import { formatNumber } from 'lib/format'; +import { useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import { formatNumber } from '@/lib/format'; import { useContext } from 'react'; import { Button, diff --git a/src/app/(main)/reports/goals/GoalsReport.tsx b/src/app/(main)/reports/goals/GoalsReport.tsx index 020d7d09..ae540f3b 100644 --- a/src/app/(main)/reports/goals/GoalsReport.tsx +++ b/src/app/(main)/reports/goals/GoalsReport.tsx @@ -4,8 +4,8 @@ import Report from '../[reportId]/Report'; import ReportHeader from '../[reportId]/ReportHeader'; import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; -import Target from 'assets/target.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Target from '@/assets/target.svg'; +import { REPORT_TYPES } from '@/lib/constants'; const defaultParameters = { type: REPORT_TYPES.goals, diff --git a/src/app/(main)/reports/insights/InsightsParameters.tsx b/src/app/(main)/reports/insights/InsightsParameters.tsx index 7f58de6a..6b3402fb 100644 --- a/src/app/(main)/reports/insights/InsightsParameters.tsx +++ b/src/app/(main)/reports/insights/InsightsParameters.tsx @@ -1,4 +1,4 @@ -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { useContext } from 'react'; import { Form, FormButtons, SubmitButton } from 'react-basics'; import BaseParameters from '../[reportId]/BaseParameters'; diff --git a/src/app/(main)/reports/insights/InsightsReport.tsx b/src/app/(main)/reports/insights/InsightsReport.tsx index adfbbd20..d43576fa 100644 --- a/src/app/(main)/reports/insights/InsightsReport.tsx +++ b/src/app/(main)/reports/insights/InsightsReport.tsx @@ -4,8 +4,8 @@ import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; import InsightsParameters from './InsightsParameters'; import InsightsTable from './InsightsTable'; -import Lightbulb from 'assets/lightbulb.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Lightbulb from '@/assets/lightbulb.svg'; +import { REPORT_TYPES } from '@/lib/constants'; const defaultParameters = { type: REPORT_TYPES.insights, diff --git a/src/app/(main)/reports/insights/InsightsTable.tsx b/src/app/(main)/reports/insights/InsightsTable.tsx index 692d7824..6864d919 100644 --- a/src/app/(main)/reports/insights/InsightsTable.tsx +++ b/src/app/(main)/reports/insights/InsightsTable.tsx @@ -1,9 +1,9 @@ import { useContext, useEffect, useState } from 'react'; import { GridTable, GridColumn } from 'react-basics'; -import { useFormat, useMessages } from 'components/hooks'; +import { useFormat, useMessages } from '@/components/hooks'; import { ReportContext } from '../[reportId]/Report'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import { formatShortTime } from 'lib/format'; +import EmptyPlaceholder from '@/components/common/EmptyPlaceholder'; +import { formatShortTime } from '@/lib/format'; export function InsightsTable() { const [fields, setFields] = useState([]); diff --git a/src/app/(main)/reports/journey/JourneyParameters.tsx b/src/app/(main)/reports/journey/JourneyParameters.tsx index eca9c42d..ffa5df89 100644 --- a/src/app/(main)/reports/journey/JourneyParameters.tsx +++ b/src/app/(main)/reports/journey/JourneyParameters.tsx @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { Dropdown, Form, diff --git a/src/app/(main)/reports/journey/JourneyReport.tsx b/src/app/(main)/reports/journey/JourneyReport.tsx index 9048b3d1..4322fa2a 100644 --- a/src/app/(main)/reports/journey/JourneyReport.tsx +++ b/src/app/(main)/reports/journey/JourneyReport.tsx @@ -5,8 +5,8 @@ import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; import JourneyParameters from './JourneyParameters'; import JourneyView from './JourneyView'; -import Path from 'assets/path.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Path from '@/assets/path.svg'; +import { REPORT_TYPES } from '@/lib/constants'; const defaultParameters = { type: REPORT_TYPES.journey, diff --git a/src/app/(main)/reports/journey/JourneyView.tsx b/src/app/(main)/reports/journey/JourneyView.tsx index ae980a6e..abddf023 100644 --- a/src/app/(main)/reports/journey/JourneyView.tsx +++ b/src/app/(main)/reports/journey/JourneyView.tsx @@ -2,11 +2,11 @@ import { useContext, useMemo, useState } from 'react'; import { TextOverflow, TooltipPopup } from 'react-basics'; import { firstBy } from 'thenby'; import classNames from 'classnames'; -import { useEscapeKey, useMessages } from 'components/hooks'; -import { objectToArray } from 'lib/data'; +import { useEscapeKey, useMessages } from '@/components/hooks'; +import { objectToArray } from '@/lib/data'; import { ReportContext } from '../[reportId]/Report'; import styles from './JourneyView.module.css'; -import { formatLongNumber } from 'lib/format'; +import { formatLongNumber } from '@/lib/format'; const NODE_HEIGHT = 60; const NODE_GAP = 10; diff --git a/src/app/(main)/reports/retention/RetentionParameters.tsx b/src/app/(main)/reports/retention/RetentionParameters.tsx index f441177c..56cbdbd3 100644 --- a/src/app/(main)/reports/retention/RetentionParameters.tsx +++ b/src/app/(main)/reports/retention/RetentionParameters.tsx @@ -1,10 +1,10 @@ import { useContext } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics'; import { ReportContext } from '../[reportId]/Report'; -import { MonthSelect } from 'components/input/MonthSelect'; +import { MonthSelect } from '@/components/input/MonthSelect'; import BaseParameters from '../[reportId]/BaseParameters'; -import { parseDateRange } from 'lib/date'; +import { parseDateRange } from '@/lib/date'; export function RetentionParameters() { const { report, runReport, isRunning, updateReport } = useContext(ReportContext); diff --git a/src/app/(main)/reports/retention/RetentionReport.tsx b/src/app/(main)/reports/retention/RetentionReport.tsx index 81b565dc..054a1a66 100644 --- a/src/app/(main)/reports/retention/RetentionReport.tsx +++ b/src/app/(main)/reports/retention/RetentionReport.tsx @@ -4,9 +4,9 @@ import Report from '../[reportId]/Report'; import ReportHeader from '../[reportId]/ReportHeader'; import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; -import Magnet from 'assets/magnet.svg'; -import { REPORT_TYPES } from 'lib/constants'; -import { parseDateRange } from 'lib/date'; +import Magnet from '@/assets/magnet.svg'; +import { REPORT_TYPES } from '@/lib/constants'; +import { parseDateRange } from '@/lib/date'; import { endOfMonth, startOfMonth } from 'date-fns'; const defaultParameters = { diff --git a/src/app/(main)/reports/retention/RetentionTable.tsx b/src/app/(main)/reports/retention/RetentionTable.tsx index 6d825567..23f0a8b0 100644 --- a/src/app/(main)/reports/retention/RetentionTable.tsx +++ b/src/app/(main)/reports/retention/RetentionTable.tsx @@ -1,9 +1,9 @@ import { useContext } from 'react'; import classNames from 'classnames'; import { ReportContext } from '../[reportId]/Report'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import { useMessages, useLocale } from 'components/hooks'; -import { formatDate } from 'lib/date'; +import EmptyPlaceholder from '@/components/common/EmptyPlaceholder'; +import { useMessages, useLocale } from '@/components/hooks'; +import { formatDate } from '@/lib/date'; import styles from './RetentionTable.module.css'; const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28]; diff --git a/src/app/(main)/reports/revenue/RevenueParameters.tsx b/src/app/(main)/reports/revenue/RevenueParameters.tsx index 8e344e4d..5cee14de 100644 --- a/src/app/(main)/reports/revenue/RevenueParameters.tsx +++ b/src/app/(main)/reports/revenue/RevenueParameters.tsx @@ -1,5 +1,5 @@ -import { useMessages } from 'components/hooks'; -import useRevenueValues from 'components/hooks/queries/useRevenueValues'; +import { useMessages } from '@/components/hooks'; +import useRevenueValues from '@/components/hooks/queries/useRevenueValues'; import { useContext } from 'react'; import { Dropdown, Form, FormButtons, FormInput, FormRow, Item, SubmitButton } from 'react-basics'; import BaseParameters from '../[reportId]/BaseParameters'; diff --git a/src/app/(main)/reports/revenue/RevenueReport.tsx b/src/app/(main)/reports/revenue/RevenueReport.tsx index 7b75ebd2..8400c651 100644 --- a/src/app/(main)/reports/revenue/RevenueReport.tsx +++ b/src/app/(main)/reports/revenue/RevenueReport.tsx @@ -1,5 +1,5 @@ -import Money from 'assets/money.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Money from '@/assets/money.svg'; +import { REPORT_TYPES } from '@/lib/constants'; import Report from '../[reportId]/Report'; import ReportBody from '../[reportId]/ReportBody'; import ReportHeader from '../[reportId]/ReportHeader'; diff --git a/src/app/(main)/reports/revenue/RevenueTable.tsx b/src/app/(main)/reports/revenue/RevenueTable.tsx index 0b9fcdc3..184797e9 100644 --- a/src/app/(main)/reports/revenue/RevenueTable.tsx +++ b/src/app/(main)/reports/revenue/RevenueTable.tsx @@ -1,9 +1,9 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import { useMessages } from 'components/hooks'; +import EmptyPlaceholder from '@/components/common/EmptyPlaceholder'; +import { useMessages } from '@/components/hooks'; import { useContext } from 'react'; import { GridColumn, GridTable } from 'react-basics'; import { ReportContext } from '../[reportId]/Report'; -import { formatLongCurrency } from 'lib/format'; +import { formatLongCurrency } from '@/lib/format'; export function RevenueTable() { const { report } = useContext(ReportContext); diff --git a/src/app/(main)/reports/revenue/RevenueView.tsx b/src/app/(main)/reports/revenue/RevenueView.tsx index 2d559893..bd3d6c63 100644 --- a/src/app/(main)/reports/revenue/RevenueView.tsx +++ b/src/app/(main)/reports/revenue/RevenueView.tsx @@ -1,16 +1,16 @@ import classNames from 'classnames'; import { colord } from 'colord'; -import BarChart from 'components/charts/BarChart'; -import PieChart from 'components/charts/PieChart'; -import TypeIcon from 'components/common/TypeIcon'; -import { useCountryNames, useLocale, useMessages } from 'components/hooks'; -import { GridRow } from 'components/layout/Grid'; -import ListTable from 'components/metrics/ListTable'; -import MetricCard from 'components/metrics/MetricCard'; -import MetricsBar from 'components/metrics/MetricsBar'; -import { renderDateLabels } from 'lib/charts'; -import { CHART_COLORS } from 'lib/constants'; -import { formatLongCurrency, formatLongNumber } from 'lib/format'; +import BarChart from '@/components/charts/BarChart'; +import PieChart from '@/components/charts/PieChart'; +import TypeIcon from '@/components/common/TypeIcon'; +import { useCountryNames, useLocale, useMessages } from '@/components/hooks'; +import { GridRow } from '@/components/layout/Grid'; +import ListTable from '@/components/metrics/ListTable'; +import MetricCard from '@/components/metrics/MetricCard'; +import MetricsBar from '@/components/metrics/MetricsBar'; +import { renderDateLabels } from '@/lib/charts'; +import { CHART_COLORS } from '@/lib/constants'; +import { formatLongCurrency, formatLongNumber } from '@/lib/format'; import { useCallback, useContext, useMemo } from 'react'; import { ReportContext } from '../[reportId]/Report'; import RevenueTable from './RevenueTable'; diff --git a/src/app/(main)/reports/utm/UTMParameters.tsx b/src/app/(main)/reports/utm/UTMParameters.tsx index c76df77d..5ae6017f 100644 --- a/src/app/(main)/reports/utm/UTMParameters.tsx +++ b/src/app/(main)/reports/utm/UTMParameters.tsx @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import { Form, FormButtons, SubmitButton } from 'react-basics'; import { ReportContext } from '../[reportId]/Report'; import BaseParameters from '../[reportId]/BaseParameters'; diff --git a/src/app/(main)/reports/utm/UTMReport.tsx b/src/app/(main)/reports/utm/UTMReport.tsx index 7183b9f7..d9d2f579 100644 --- a/src/app/(main)/reports/utm/UTMReport.tsx +++ b/src/app/(main)/reports/utm/UTMReport.tsx @@ -5,8 +5,8 @@ import ReportMenu from '../[reportId]/ReportMenu'; import ReportBody from '../[reportId]/ReportBody'; import UTMParameters from './UTMParameters'; import UTMView from './UTMView'; -import Tag from 'assets/tag.svg'; -import { REPORT_TYPES } from 'lib/constants'; +import Tag from '@/assets/tag.svg'; +import { REPORT_TYPES } from '@/lib/constants'; const defaultParameters = { type: REPORT_TYPES.utm, diff --git a/src/app/(main)/reports/utm/UTMView.tsx b/src/app/(main)/reports/utm/UTMView.tsx index f10a68d8..ba025824 100644 --- a/src/app/(main)/reports/utm/UTMView.tsx +++ b/src/app/(main)/reports/utm/UTMView.tsx @@ -1,11 +1,11 @@ import { useContext } from 'react'; import { firstBy } from 'thenby'; import { ReportContext } from '../[reportId]/Report'; -import { CHART_COLORS, UTM_PARAMS } from 'lib/constants'; -import PieChart from 'components/charts/PieChart'; -import ListTable from 'components/metrics/ListTable'; +import { CHART_COLORS, UTM_PARAMS } from '@/lib/constants'; +import PieChart from '@/components/charts/PieChart'; +import ListTable from '@/components/metrics/ListTable'; import styles from './UTMView.module.css'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; function toArray(data: { [key: string]: number } = {}) { return Object.keys(data) diff --git a/src/app/(main)/settings/SettingsLayout.tsx b/src/app/(main)/settings/SettingsLayout.tsx index 28e9a074..08dcc3eb 100644 --- a/src/app/(main)/settings/SettingsLayout.tsx +++ b/src/app/(main)/settings/SettingsLayout.tsx @@ -1,7 +1,7 @@ 'use client'; import { ReactNode } from 'react'; -import { useLogin, useMessages } from 'components/hooks'; -import MenuLayout from 'components/layout/MenuLayout'; +import { useLogin, useMessages } from '@/components/hooks'; +import MenuLayout from '@/components/layout/MenuLayout'; export default function SettingsLayout({ children }: { children: ReactNode }) { const { user } = useLogin(); diff --git a/src/app/(main)/settings/teams/TeamAddForm.tsx b/src/app/(main)/settings/teams/TeamAddForm.tsx index c0fc7513..e940aa17 100644 --- a/src/app/(main)/settings/teams/TeamAddForm.tsx +++ b/src/app/(main)/settings/teams/TeamAddForm.tsx @@ -1,4 +1,4 @@ -import { useApi, useMessages } from 'components/hooks'; +import { useApi, useMessages } from '@/components/hooks'; import { Button, Form, diff --git a/src/app/(main)/settings/teams/TeamJoinForm.tsx b/src/app/(main)/settings/teams/TeamJoinForm.tsx index 939b5d4b..78be872c 100644 --- a/src/app/(main)/settings/teams/TeamJoinForm.tsx +++ b/src/app/(main)/settings/teams/TeamJoinForm.tsx @@ -8,10 +8,10 @@ import { Button, SubmitButton, } from 'react-basics'; -import { useApi, useMessages, useModified } from 'components/hooks'; +import { useApi, useMessages, useModified } from '@/components/hooks'; export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { - const { formatMessage, labels, getMessage } = useMessages(); + const { formatMessage, labels } = useMessages(); const { post, useMutation } = useApi(); const { mutate, error } = useMutation({ mutationFn: (data: any) => post('/teams/join', data) }); const ref = useRef(null); @@ -28,7 +28,7 @@ export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: }; return ( -
+ diff --git a/src/app/(main)/settings/teams/TeamLeaveButton.tsx b/src/app/(main)/settings/teams/TeamLeaveButton.tsx index b8a24c7e..5f5b54f3 100644 --- a/src/app/(main)/settings/teams/TeamLeaveButton.tsx +++ b/src/app/(main)/settings/teams/TeamLeaveButton.tsx @@ -1,4 +1,4 @@ -import { useLocale, useLogin, useMessages, useModified } from 'components/hooks'; +import { useLocale, useLogin, useMessages, useModified } from '@/components/hooks'; import { useRouter } from 'next/navigation'; import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; import TeamDeleteForm from './TeamLeaveForm'; diff --git a/src/app/(main)/settings/teams/TeamLeaveForm.tsx b/src/app/(main)/settings/teams/TeamLeaveForm.tsx index 8c9726be..daf46434 100644 --- a/src/app/(main)/settings/teams/TeamLeaveForm.tsx +++ b/src/app/(main)/settings/teams/TeamLeaveForm.tsx @@ -1,5 +1,5 @@ -import { useApi, useMessages, useModified } from 'components/hooks'; -import ConfirmationForm from 'components/common/ConfirmationForm'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import ConfirmationForm from '@/components/common/ConfirmationForm'; export function TeamLeaveForm({ teamId, @@ -14,7 +14,7 @@ export function TeamLeaveForm({ onSave: () => void; onClose: () => void; }) { - const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { formatMessage, labels, messages } = useMessages(); const { del, useMutation } = useApi(); const { mutate, error, isPending } = useMutation({ mutationFn: () => del(`/teams/${teamId}/users/${userId}`), @@ -34,9 +34,7 @@ export function TeamLeaveForm({ return ( {teamName} }} /> - } + message={formatMessage(messages.confirmLeave, { target: {teamName} })} onConfirm={handleConfirm} onClose={onClose} isLoading={isPending} diff --git a/src/app/(main)/settings/teams/TeamsAddButton.tsx b/src/app/(main)/settings/teams/TeamsAddButton.tsx index 6ec4cf39..58c138a8 100644 --- a/src/app/(main)/settings/teams/TeamsAddButton.tsx +++ b/src/app/(main)/settings/teams/TeamsAddButton.tsx @@ -1,8 +1,8 @@ import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import Icons from 'components/icons'; -import { useMessages, useModified } from 'components/hooks'; +import Icons from '@/components/icons'; +import { useMessages, useModified } from '@/components/hooks'; import TeamAddForm from './TeamAddForm'; -import { messages } from 'components/messages'; +import { messages } from '@/components/messages'; export function TeamsAddButton({ onSave }: { onSave?: () => void }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/settings/teams/TeamsDataTable.tsx b/src/app/(main)/settings/teams/TeamsDataTable.tsx index e8563b2e..9b8c9b27 100644 --- a/src/app/(main)/settings/teams/TeamsDataTable.tsx +++ b/src/app/(main)/settings/teams/TeamsDataTable.tsx @@ -1,6 +1,6 @@ -import DataTable from 'components/common/DataTable'; -import TeamsTable from 'app/(main)/settings/teams/TeamsTable'; -import { useLogin, useTeams } from 'components/hooks'; +import DataTable from '@/components/common/DataTable'; +import TeamsTable from '@/app/(main)/settings/teams/TeamsTable'; +import { useLogin, useTeams } from '@/components/hooks'; import { ReactNode } from 'react'; export function TeamsDataTable({ diff --git a/src/app/(main)/settings/teams/TeamsHeader.tsx b/src/app/(main)/settings/teams/TeamsHeader.tsx index 8aa5e7a3..e1911a19 100644 --- a/src/app/(main)/settings/teams/TeamsHeader.tsx +++ b/src/app/(main)/settings/teams/TeamsHeader.tsx @@ -1,7 +1,7 @@ import { Flexbox } from 'react-basics'; -import PageHeader from 'components/layout/PageHeader'; -import { ROLES } from 'lib/constants'; -import { useLogin, useMessages } from 'components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; +import { ROLES } from '@/lib/constants'; +import { useLogin, useMessages } from '@/components/hooks'; import TeamsJoinButton from './TeamsJoinButton'; import TeamsAddButton from './TeamsAddButton'; diff --git a/src/app/(main)/settings/teams/TeamsJoinButton.tsx b/src/app/(main)/settings/teams/TeamsJoinButton.tsx index 24925bb6..bbf2d685 100644 --- a/src/app/(main)/settings/teams/TeamsJoinButton.tsx +++ b/src/app/(main)/settings/teams/TeamsJoinButton.tsx @@ -1,6 +1,6 @@ import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import Icons from 'components/icons'; -import { useMessages, useModified } from 'components/hooks'; +import Icons from '@/components/icons'; +import { useMessages, useModified } from '@/components/hooks'; import TeamJoinForm from './TeamJoinForm'; export function TeamsJoinButton() { diff --git a/src/app/(main)/settings/teams/TeamsTable.tsx b/src/app/(main)/settings/teams/TeamsTable.tsx index a7a03958..8e7efa27 100644 --- a/src/app/(main)/settings/teams/TeamsTable.tsx +++ b/src/app/(main)/settings/teams/TeamsTable.tsx @@ -1,8 +1,8 @@ import { GridColumn, GridTable, Icon, Text } from 'react-basics'; -import { useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import { ROLES } from 'lib/constants'; -import LinkButton from 'components/common/LinkButton'; +import { useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import { ROLES } from '@/lib/constants'; +import LinkButton from '@/components/common/LinkButton'; export function TeamsTable({ data = [], diff --git a/src/app/(main)/settings/users/UserAddButton.tsx b/src/app/(main)/settings/users/UserAddButton.tsx index 832cf75b..e1b04842 100644 --- a/src/app/(main)/settings/users/UserAddButton.tsx +++ b/src/app/(main)/settings/users/UserAddButton.tsx @@ -1,6 +1,6 @@ import { Button, Icon, Text, Modal, Icons, ModalTrigger, useToasts } from 'react-basics'; import UserAddForm from './UserAddForm'; -import { useMessages, useModified } from 'components/hooks'; +import { useMessages, useModified } from '@/components/hooks'; export function UserAddButton({ onSave }: { onSave?: () => void }) { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/settings/users/UserAddForm.tsx b/src/app/(main)/settings/users/UserAddForm.tsx index 979f399f..13f2faf5 100644 --- a/src/app/(main)/settings/users/UserAddForm.tsx +++ b/src/app/(main)/settings/users/UserAddForm.tsx @@ -10,8 +10,8 @@ import { SubmitButton, Button, } from 'react-basics'; -import { useApi, useMessages } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import { useApi, useMessages } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; export function UserAddForm({ onSave, onClose }) { const { post, useMutation } = useApi(); diff --git a/src/app/(main)/settings/users/UserDeleteButton.tsx b/src/app/(main)/settings/users/UserDeleteButton.tsx index 9f1f8459..0909720e 100644 --- a/src/app/(main)/settings/users/UserDeleteButton.tsx +++ b/src/app/(main)/settings/users/UserDeleteButton.tsx @@ -1,5 +1,5 @@ import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import { useMessages, useLogin } from 'components/hooks'; +import { useMessages, useLogin } from '@/components/hooks'; import UserDeleteForm from './UserDeleteForm'; export function UserDeleteButton({ diff --git a/src/app/(main)/settings/users/UserDeleteForm.tsx b/src/app/(main)/settings/users/UserDeleteForm.tsx index 9b49647f..3ac7c118 100644 --- a/src/app/(main)/settings/users/UserDeleteForm.tsx +++ b/src/app/(main)/settings/users/UserDeleteForm.tsx @@ -1,8 +1,8 @@ -import { useApi, useMessages, useModified } from 'components/hooks'; -import ConfirmationForm from 'components/common/ConfirmationForm'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import ConfirmationForm from '@/components/common/ConfirmationForm'; export function UserDeleteForm({ userId, username, onSave, onClose }) { - const { FormattedMessage, messages, labels, formatMessage } = useMessages(); + const { messages, labels, formatMessage } = useMessages(); const { del, useMutation } = useApi(); const { mutate, error, isPending } = useMutation({ mutationFn: () => del(`/users/${userId}`) }); const { touch } = useModified(); @@ -19,9 +19,7 @@ export function UserDeleteForm({ userId, username, onSave, onClose }) { return ( {username} }} /> - } + message={formatMessage(messages.confirmDelete, { target: {username} })} onConfirm={handleConfirm} onClose={onClose} buttonLabel={formatMessage(labels.delete)} diff --git a/src/app/(main)/settings/users/UsersDataTable.tsx b/src/app/(main)/settings/users/UsersDataTable.tsx index 03addc74..867f4090 100644 --- a/src/app/(main)/settings/users/UsersDataTable.tsx +++ b/src/app/(main)/settings/users/UsersDataTable.tsx @@ -1,5 +1,5 @@ -import DataTable from 'components/common/DataTable'; -import { useUsers } from 'components/hooks'; +import DataTable from '@/components/common/DataTable'; +import { useUsers } from '@/components/hooks'; import UsersTable from './UsersTable'; import { ReactNode } from 'react'; diff --git a/src/app/(main)/settings/users/UsersHeader.tsx b/src/app/(main)/settings/users/UsersHeader.tsx index 6f4387c7..d07a159f 100644 --- a/src/app/(main)/settings/users/UsersHeader.tsx +++ b/src/app/(main)/settings/users/UsersHeader.tsx @@ -1,5 +1,5 @@ -import PageHeader from 'components/layout/PageHeader'; -import { useMessages } from 'components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; +import { useMessages } from '@/components/hooks'; import UserAddButton from './UserAddButton'; export function UsersHeader({ onAdd }: { onAdd?: () => void }) { diff --git a/src/app/(main)/settings/users/UsersTable.tsx b/src/app/(main)/settings/users/UsersTable.tsx index e074be24..c698f38b 100644 --- a/src/app/(main)/settings/users/UsersTable.tsx +++ b/src/app/(main)/settings/users/UsersTable.tsx @@ -1,9 +1,9 @@ import { Text, Icon, Icons, GridTable, GridColumn } from 'react-basics'; import { formatDistance } from 'date-fns'; -import { ROLES } from 'lib/constants'; -import { useMessages, useLocale } from 'components/hooks'; +import { ROLES } from '@/lib/constants'; +import { useMessages, useLocale } from '@/components/hooks'; import UserDeleteButton from './UserDeleteButton'; -import LinkButton from 'components/common/LinkButton'; +import LinkButton from '@/components/common/LinkButton'; export function UsersTable({ data = [], diff --git a/src/app/(main)/settings/users/[userId]/UserEditForm.tsx b/src/app/(main)/settings/users/[userId]/UserEditForm.tsx index 1acfc581..70f21f63 100644 --- a/src/app/(main)/settings/users/[userId]/UserEditForm.tsx +++ b/src/app/(main)/settings/users/[userId]/UserEditForm.tsx @@ -9,8 +9,8 @@ import { SubmitButton, PasswordField, } from 'react-basics'; -import { useApi, useLogin, useMessages } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import { useApi, useLogin, useMessages } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; import { useContext, useRef } from 'react'; import { UserContext } from './UserProvider'; diff --git a/src/app/(main)/settings/users/[userId]/UserProvider.tsx b/src/app/(main)/settings/users/[userId]/UserProvider.tsx index b289fca0..ed559c91 100644 --- a/src/app/(main)/settings/users/[userId]/UserProvider.tsx +++ b/src/app/(main)/settings/users/[userId]/UserProvider.tsx @@ -1,5 +1,5 @@ import { createContext, ReactNode, useEffect } from 'react'; -import { useModified, useUser } from 'components/hooks'; +import { useModified, useUser } from '@/components/hooks'; import { Loading } from 'react-basics'; export const UserContext = createContext(null); diff --git a/src/app/(main)/settings/users/[userId]/UserSettings.tsx b/src/app/(main)/settings/users/[userId]/UserSettings.tsx index f9e17a85..0d98205f 100644 --- a/src/app/(main)/settings/users/[userId]/UserSettings.tsx +++ b/src/app/(main)/settings/users/[userId]/UserSettings.tsx @@ -1,12 +1,12 @@ import { Key, useContext, useState } from 'react'; import { Item, Tabs, useToasts } from 'react-basics'; -import Icons from 'components/icons'; +import Icons from '@/components/icons'; import UserEditForm from './UserEditForm'; -import PageHeader from 'components/layout/PageHeader'; -import { useMessages } from 'components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; +import { useMessages } from '@/components/hooks'; import UserWebsites from './UserWebsites'; import { UserContext } from './UserProvider'; -import Breadcrumb from 'components/common/Breadcrumb'; +import Breadcrumb from '@/components/common/Breadcrumb'; export function UserSettings({ userId }: { userId: string }) { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/settings/users/[userId]/UserWebsites.tsx b/src/app/(main)/settings/users/[userId]/UserWebsites.tsx index bfc6f74b..15521b17 100644 --- a/src/app/(main)/settings/users/[userId]/UserWebsites.tsx +++ b/src/app/(main)/settings/users/[userId]/UserWebsites.tsx @@ -1,6 +1,6 @@ -import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable'; -import DataTable from 'components/common/DataTable'; -import { useWebsites } from 'components/hooks'; +import WebsitesTable from '@/app/(main)/settings/websites/WebsitesTable'; +import DataTable from '@/components/common/DataTable'; +import { useWebsites } from '@/components/hooks'; export function UserWebsites({ userId }) { const queryResult = useWebsites({ userId }); diff --git a/src/app/(main)/settings/websites/WebsiteAddButton.tsx b/src/app/(main)/settings/websites/WebsiteAddButton.tsx index e534461c..6f32fc9f 100644 --- a/src/app/(main)/settings/websites/WebsiteAddButton.tsx +++ b/src/app/(main)/settings/websites/WebsiteAddButton.tsx @@ -1,4 +1,4 @@ -import { useMessages, useModified } from 'components/hooks'; +import { useMessages, useModified } from '@/components/hooks'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; import WebsiteAddForm from './WebsiteAddForm'; diff --git a/src/app/(main)/settings/websites/WebsiteAddForm.tsx b/src/app/(main)/settings/websites/WebsiteAddForm.tsx index 2bb4a0a8..90672412 100644 --- a/src/app/(main)/settings/websites/WebsiteAddForm.tsx +++ b/src/app/(main)/settings/websites/WebsiteAddForm.tsx @@ -7,9 +7,9 @@ import { Button, SubmitButton, } from 'react-basics'; -import { useApi } from 'components/hooks'; -import { DOMAIN_REGEX } from 'lib/constants'; -import { useMessages } from 'components/hooks'; +import { useApi } from '@/components/hooks'; +import { DOMAIN_REGEX } from '@/lib/constants'; +import { useMessages } from '@/components/hooks'; export function WebsiteAddForm({ teamId, diff --git a/src/app/(main)/settings/websites/WebsitesDataTable.tsx b/src/app/(main)/settings/websites/WebsitesDataTable.tsx index d91bbeef..023df857 100644 --- a/src/app/(main)/settings/websites/WebsitesDataTable.tsx +++ b/src/app/(main)/settings/websites/WebsitesDataTable.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react'; -import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable'; -import DataTable from 'components/common/DataTable'; -import { useWebsites } from 'components/hooks'; +import WebsitesTable from '@/app/(main)/settings/websites/WebsitesTable'; +import DataTable from '@/components/common/DataTable'; +import { useWebsites } from '@/components/hooks'; export function WebsitesDataTable({ teamId, diff --git a/src/app/(main)/settings/websites/WebsitesHeader.tsx b/src/app/(main)/settings/websites/WebsitesHeader.tsx index 6f322371..34e87a13 100644 --- a/src/app/(main)/settings/websites/WebsitesHeader.tsx +++ b/src/app/(main)/settings/websites/WebsitesHeader.tsx @@ -1,5 +1,5 @@ -import { useMessages } from 'components/hooks'; -import PageHeader from 'components/layout/PageHeader'; +import { useMessages } from '@/components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; import WebsiteAddButton from './WebsiteAddButton'; export interface WebsitesHeaderProps { diff --git a/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx b/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx index ff0938d1..61909a9e 100644 --- a/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx +++ b/src/app/(main)/settings/websites/WebsitesSettingsPage.tsx @@ -1,8 +1,8 @@ 'use client'; -import { useLogin } from 'components/hooks'; +import { useLogin } from '@/components/hooks'; import WebsitesDataTable from './WebsitesDataTable'; import WebsitesHeader from './WebsitesHeader'; -import { ROLES } from 'lib/constants'; +import { ROLES } from '@/lib/constants'; export default function WebsitesSettingsPage({ teamId }: { teamId: string }) { const { user } = useLogin(); diff --git a/src/app/(main)/settings/websites/WebsitesTable.tsx b/src/app/(main)/settings/websites/WebsitesTable.tsx index 5e9aef2c..79749b97 100644 --- a/src/app/(main)/settings/websites/WebsitesTable.tsx +++ b/src/app/(main)/settings/websites/WebsitesTable.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react'; import { Text, Icon, Icons, GridTable, GridColumn } from 'react-basics'; -import { useMessages, useTeamUrl } from 'components/hooks'; -import LinkButton from 'components/common/LinkButton'; +import { useMessages, useTeamUrl } from '@/components/hooks'; +import LinkButton from '@/components/common/LinkButton'; export interface WebsitesTableProps { data: any[]; diff --git a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx index e5673346..318e4e95 100644 --- a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx @@ -9,9 +9,9 @@ import { LoadingButton, } from 'react-basics'; import { useContext, useState } from 'react'; -import { getRandomChars } from 'next-basics'; -import { useApi, useMessages, useModified } from 'components/hooks'; -import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; +import { getRandomChars } from '@/lib/crypto'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; const generateId = () => getRandomChars(16); @@ -35,7 +35,11 @@ export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () => }; const handleCheck = (checked: boolean) => { - const data = { shareId: checked ? generateId() : null }; + const data = { + name: website.name, + domain: website.domain, + shareId: checked ? generateId() : null, + }; mutate(data, { onSuccess: async () => { touch(`website:${website.id}`); @@ -47,7 +51,7 @@ export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () => const handleSave = () => { mutate( - { shareId: id }, + { name: website.name, domain: website.domain, shareId: id }, { onSuccess: async () => { touch(`website:${website.id}`); diff --git a/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx b/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx index 95fb7068..cacdf689 100644 --- a/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx @@ -1,5 +1,5 @@ import { TextArea } from 'react-basics'; -import { useMessages, useConfig } from 'components/hooks'; +import { useMessages, useConfig } from '@/components/hooks'; const SCRIPT_NAME = 'script.js'; diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx index bc6a3169..d11f24df 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteData.tsx @@ -1,10 +1,10 @@ import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics'; import { useRouter } from 'next/navigation'; -import { useLogin, useMessages, useModified, useTeams, useTeamUrl } from 'components/hooks'; +import { useLogin, useMessages, useModified, useTeams, useTeamUrl } from '@/components/hooks'; import WebsiteDeleteForm from './WebsiteDeleteForm'; import WebsiteResetForm from './WebsiteResetForm'; import WebsiteTransferForm from './WebsiteTransferForm'; -import { ROLES } from 'lib/constants'; +import { ROLES } from '@/lib/constants'; export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx index 077a8f4a..5eef3544 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm.tsx @@ -1,5 +1,5 @@ -import { useApi, useMessages } from 'components/hooks'; -import TypeConfirmationForm from 'components/common/TypeConfirmationForm'; +import { useApi, useMessages } from '@/components/hooks'; +import TypeConfirmationForm from '@/components/common/TypeConfirmationForm'; const CONFIRM_VALUE = 'DELETE'; diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteEditForm.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteEditForm.tsx index 15538661..aeef7f34 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteEditForm.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteEditForm.tsx @@ -1,8 +1,8 @@ import { useContext, useRef } from 'react'; import { SubmitButton, Form, FormInput, FormRow, FormButtons, TextField } from 'react-basics'; -import { useApi, useMessages, useModified } from 'components/hooks'; -import { DOMAIN_REGEX } from 'lib/constants'; -import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import { DOMAIN_REGEX } from '@/lib/constants'; +import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) { const website = useContext(WebsiteContext); diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteResetForm.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteResetForm.tsx index c43f3efb..73886aa9 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteResetForm.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteResetForm.tsx @@ -1,5 +1,5 @@ -import { useApi, useMessages } from 'components/hooks'; -import TypeConfirmationForm from 'components/common/TypeConfirmationForm'; +import { useApi, useMessages } from '@/components/hooks'; +import TypeConfirmationForm from '@/components/common/TypeConfirmationForm'; const CONFIRM_VALUE = 'RESET'; diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx index 11f662b1..5bea2704 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx @@ -1,8 +1,8 @@ -import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; -import Breadcrumb from 'components/common/Breadcrumb'; -import { useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import PageHeader from 'components/layout/PageHeader'; +import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; +import Breadcrumb from '@/components/common/Breadcrumb'; +import { useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import PageHeader from '@/components/layout/PageHeader'; import Link from 'next/link'; import { Key, useContext, useState } from 'react'; import { Button, Icon, Item, Tabs, Text, useToasts } from 'react-basics'; diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx index 00147629..8d7badb8 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettingsPage.tsx @@ -1,5 +1,5 @@ 'use client'; -import WebsiteProvider from 'app/(main)/websites/[websiteId]/WebsiteProvider'; +import WebsiteProvider from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; import WebsiteSettings from './WebsiteSettings'; export default function WebsiteSettingsPage({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteTransferForm.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteTransferForm.tsx index eb568a7f..8214fb16 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteTransferForm.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteTransferForm.tsx @@ -10,9 +10,9 @@ import { Item, Flexbox, } from 'react-basics'; -import { useApi, useLogin, useMessages, useTeams } from 'components/hooks'; -import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; -import { ROLES } from 'lib/constants'; +import { useApi, useLogin, useMessages, useTeams } from '@/components/hooks'; +import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; +import { ROLES } from '@/lib/constants'; export function WebsiteTransferForm({ websiteId, @@ -71,7 +71,8 @@ export function WebsiteTransferForm({ {result.data .filter(({ teamUser }) => teamUser.find( - ({ role, userId }) => [ ROLES.teamOwner, ROLES.teamManager ].includes(role) && userId === user.id, + ({ role, userId }) => + [ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id, ), ) .map(({ id, name }) => { diff --git a/src/app/(main)/teams/[teamId]/TeamProvider.tsx b/src/app/(main)/teams/[teamId]/TeamProvider.tsx index 467a5d22..ed2d5467 100644 --- a/src/app/(main)/teams/[teamId]/TeamProvider.tsx +++ b/src/app/(main)/teams/[teamId]/TeamProvider.tsx @@ -1,6 +1,6 @@ 'use client'; import { createContext, ReactNode, useEffect } from 'react'; -import { useTeam, useModified } from 'components/hooks'; +import { useTeam, useModified } from '@/components/hooks'; import { Loading } from 'react-basics'; export const TeamContext = createContext(null); diff --git a/src/app/(main)/teams/[teamId]/settings/TeamSettingsLayout.tsx b/src/app/(main)/teams/[teamId]/settings/TeamSettingsLayout.tsx index f7df620a..8c638d29 100644 --- a/src/app/(main)/teams/[teamId]/settings/TeamSettingsLayout.tsx +++ b/src/app/(main)/teams/[teamId]/settings/TeamSettingsLayout.tsx @@ -1,7 +1,7 @@ 'use client'; import { ReactNode } from 'react'; -import { useMessages, useTeamUrl } from 'components/hooks'; -import MenuLayout from 'components/layout/MenuLayout'; +import { useMessages, useTeamUrl } from '@/components/hooks'; +import MenuLayout from '@/components/layout/MenuLayout'; export default function TeamSettingsLayout({ children }: { children: ReactNode }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton.tsx index 9cc99869..85292f60 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton.tsx @@ -1,4 +1,4 @@ -import { useMessages, useModified } from 'components/hooks'; +import { useMessages, useModified } from '@/components/hooks'; import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; import TeamMemberEditForm from './TeamMemberEditForm'; diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm.tsx index 40183989..4ce605df 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm.tsx @@ -1,5 +1,5 @@ -import { useApi, useMessages } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import { useApi, useMessages } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; import { Button, Dropdown, diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx index 3f024395..931390c7 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx @@ -1,8 +1,7 @@ -import ConfirmationForm from 'components/common/ConfirmationForm'; -import { useApi, useMessages, useModified } from 'components/hooks'; -import { messages } from 'components/messages'; +import ConfirmationForm from '@/components/common/ConfirmationForm'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import { messages } from '@/components/messages'; import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import { FormattedMessage } from 'react-intl'; export function TeamMemberRemoveButton({ teamId, @@ -44,12 +43,7 @@ export function TeamMemberRemoveButton({ {(close: () => void) => ( {userName} }} - /> - } + message={formatMessage(messages.confirmRemove, { target: {userName} })} isLoading={isPending} error={error} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable.tsx index 996283a7..9de26415 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable.tsx @@ -1,6 +1,6 @@ -import DataTable from 'components/common/DataTable'; +import DataTable from '@/components/common/DataTable'; import TeamMembersTable from './TeamMembersTable'; -import { useTeamMembers } from 'components/hooks'; +import { useTeamMembers } from '@/components/hooks'; export function TeamMembersDataTable({ teamId, diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersPage.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersPage.tsx index de0c4c0a..557a40ba 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersPage.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersPage.tsx @@ -1,9 +1,9 @@ 'use client'; -import { TeamContext } from 'app/(main)/teams/[teamId]/TeamProvider'; +import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; import TeamMembersDataTable from './TeamMembersDataTable'; -import PageHeader from 'components/layout/PageHeader'; -import { useLogin, useMessages } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import PageHeader from '@/components/layout/PageHeader'; +import { useLogin, useMessages } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; import { useContext } from 'react'; export function TeamMembersPage({ teamId }: { teamId: string }) { diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersTable.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersTable.tsx index 67cb23c7..0054437a 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMembersTable.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMembersTable.tsx @@ -1,6 +1,6 @@ import { GridColumn, GridTable } from 'react-basics'; -import { useMessages, useLogin } from 'components/hooks'; -import { ROLES } from 'lib/constants'; +import { useMessages, useLogin } from '@/components/hooks'; +import { ROLES } from '@/lib/constants'; import TeamMemberRemoveButton from './TeamMemberRemoveButton'; import TeamMemberEditButton from './TeamMemberEditButton'; diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm.tsx b/src/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm.tsx index 3cbdf550..5e7f5cf8 100644 --- a/src/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm.tsx +++ b/src/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm.tsx @@ -1,5 +1,5 @@ -import TypeConfirmationForm from 'components/common/TypeConfirmationForm'; -import { useApi, useMessages } from 'components/hooks'; +import TypeConfirmationForm from '@/components/common/TypeConfirmationForm'; +import { useApi, useMessages } from '@/components/hooks'; const CONFIRM_VALUE = 'DELETE'; diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamDetails.tsx b/src/app/(main)/teams/[teamId]/settings/team/TeamDetails.tsx index 70858ee4..f3f258bd 100644 --- a/src/app/(main)/teams/[teamId]/settings/team/TeamDetails.tsx +++ b/src/app/(main)/teams/[teamId]/settings/team/TeamDetails.tsx @@ -1,11 +1,11 @@ -import { TeamContext } from 'app/(main)/teams/[teamId]/TeamProvider'; -import { useLogin, useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import PageHeader from 'components/layout/PageHeader'; -import { ROLES } from 'lib/constants'; +import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; +import { useLogin, useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import PageHeader from '@/components/layout/PageHeader'; +import { ROLES } from '@/lib/constants'; import { useContext, useState } from 'react'; import { Flexbox, Item, Tabs } from 'react-basics'; -import TeamLeaveButton from 'app/(main)/settings/teams/TeamLeaveButton'; +import TeamLeaveButton from '@/app/(main)/settings/teams/TeamLeaveButton'; import TeamManage from './TeamManage'; import TeamEditForm from './TeamEditForm'; diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamEditForm.tsx b/src/app/(main)/teams/[teamId]/settings/team/TeamEditForm.tsx index c2029ca6..ac158fa7 100644 --- a/src/app/(main)/teams/[teamId]/settings/team/TeamEditForm.tsx +++ b/src/app/(main)/teams/[teamId]/settings/team/TeamEditForm.tsx @@ -9,10 +9,10 @@ import { Flexbox, useToasts, } from 'react-basics'; -import { getRandomChars } from 'next-basics'; +import { getRandomChars } from '@/lib/crypto'; import { useContext, useRef, useState } from 'react'; -import { useApi, useMessages, useModified } from 'components/hooks'; -import { TeamContext } from 'app/(main)/teams/[teamId]/TeamProvider'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; const generateId = () => getRandomChars(16); diff --git a/src/app/(main)/teams/[teamId]/settings/team/TeamManage.tsx b/src/app/(main)/teams/[teamId]/settings/team/TeamManage.tsx index 40cbee04..24ca93d3 100644 --- a/src/app/(main)/teams/[teamId]/settings/team/TeamManage.tsx +++ b/src/app/(main)/teams/[teamId]/settings/team/TeamManage.tsx @@ -1,4 +1,4 @@ -import { useMessages, useModified } from 'components/hooks'; +import { useMessages, useModified } from '@/components/hooks'; import { useRouter } from 'next/navigation'; import { ActionForm, Button, Modal, ModalTrigger } from 'react-basics'; import TeamDeleteForm from './TeamDeleteForm'; diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton.tsx b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton.tsx index 336e151a..fdd76cd2 100644 --- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton.tsx +++ b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton.tsx @@ -1,4 +1,4 @@ -import { useApi, useMessages } from 'components/hooks'; +import { useApi, useMessages } from '@/components/hooks'; import { Icon, Icons, LoadingButton, Text } from 'react-basics'; export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) { diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable.tsx b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable.tsx index 9e2985d4..63aa47f5 100644 --- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable.tsx +++ b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable.tsx @@ -1,5 +1,5 @@ -import DataTable from 'components/common/DataTable'; -import { useTeamWebsites } from 'components/hooks'; +import DataTable from '@/components/common/DataTable'; +import { useTeamWebsites } from '@/components/hooks'; import TeamWebsitesTable from './TeamWebsitesTable'; export function TeamWebsitesDataTable({ diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesPage.tsx b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesPage.tsx index 882ef8ec..d46d928a 100644 --- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesPage.tsx +++ b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesPage.tsx @@ -1,10 +1,10 @@ 'use client'; -import { TeamContext } from 'app/(main)/teams/[teamId]/TeamProvider'; -import WebsiteAddButton from 'app/(main)/settings/websites/WebsiteAddButton'; -import { useLogin, useMessages } from 'components/hooks'; -import PageHeader from 'components/layout/PageHeader'; +import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; +import WebsiteAddButton from '@/app/(main)/settings/websites/WebsiteAddButton'; +import { useLogin, useMessages } from '@/components/hooks'; +import PageHeader from '@/components/layout/PageHeader'; import TeamWebsitesDataTable from './TeamWebsitesDataTable'; -import { ROLES } from 'lib/constants'; +import { ROLES } from '@/lib/constants'; import { useContext } from 'react'; export function TeamWebsitesPage({ teamId }: { teamId: string }) { diff --git a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable.tsx b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable.tsx index dc6760a6..76c343b1 100644 --- a/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable.tsx +++ b/src/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable.tsx @@ -1,7 +1,7 @@ import { GridColumn, GridTable, Icon, Text } from 'react-basics'; -import { useLogin, useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import LinkButton from 'components/common/LinkButton'; +import { useLogin, useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import LinkButton from '@/components/common/LinkButton'; export function TeamWebsitesTable({ teamId, diff --git a/src/app/(main)/teams/[teamId]/settings/websites/[websiteId]/page.tsx b/src/app/(main)/teams/[teamId]/settings/websites/[websiteId]/page.tsx index a6895296..a18f8a2e 100644 --- a/src/app/(main)/teams/[teamId]/settings/websites/[websiteId]/page.tsx +++ b/src/app/(main)/teams/[teamId]/settings/websites/[websiteId]/page.tsx @@ -1,4 +1,4 @@ -import Page from 'app/(main)/settings/websites/[websiteId]/page'; +import Page from '@/app/(main)/settings/websites/[websiteId]/page'; export default function ({ params }) { return ; diff --git a/src/app/(main)/websites/WebsitesPage.tsx b/src/app/(main)/websites/WebsitesPage.tsx index d6f8524b..b5e40b30 100644 --- a/src/app/(main)/websites/WebsitesPage.tsx +++ b/src/app/(main)/websites/WebsitesPage.tsx @@ -1,7 +1,7 @@ 'use client'; -import WebsitesHeader from 'app/(main)/settings/websites/WebsitesHeader'; -import WebsitesDataTable from 'app/(main)/settings/websites/WebsitesDataTable'; -import { useTeamUrl } from 'components/hooks'; +import WebsitesHeader from '@/app/(main)/settings/websites/WebsitesHeader'; +import WebsitesDataTable from '@/app/(main)/settings/websites/WebsitesDataTable'; +import { useTeamUrl } from '@/components/hooks'; export default function WebsitesPage() { const { teamId } = useTeamUrl(); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx index ddeba789..68192307 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; -import PageviewsChart from 'components/metrics/PageviewsChart'; -import useWebsitePageviews from 'components/hooks/queries/useWebsitePageviews'; -import { useDateRange } from 'components/hooks'; +import PageviewsChart from '@/components/metrics/PageviewsChart'; +import useWebsitePageviews from '@/components/hooks/queries/useWebsitePageviews'; +import { useDateRange } from '@/components/hooks'; export function WebsiteChart({ websiteId, diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx index e33e948a..b27f9870 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx @@ -3,10 +3,10 @@ import { useMemo } from 'react'; import { firstBy } from 'thenby'; import Link from 'next/link'; import WebsiteChart from './WebsiteChart'; -import useDashboard from 'store/dashboard'; +import useDashboard from '@/store/dashboard'; import WebsiteHeader from './WebsiteHeader'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; -import { useMessages, useLocale, useTeamUrl } from 'components/hooks'; +import { useMessages, useLocale, useTeamUrl } from '@/components/hooks'; export default function WebsiteChartList({ websites, diff --git a/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx b/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx index 3eeeb18f..460792ef 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx @@ -1,13 +1,13 @@ 'use client'; import { usePathname } from 'next/navigation'; -import FilterTags from 'components/metrics/FilterTags'; -import { useNavigation } from 'components/hooks'; +import FilterTags from '@/components/metrics/FilterTags'; +import { useNavigation } from '@/components/hooks'; import WebsiteChart from './WebsiteChart'; import WebsiteExpandedView from './WebsiteExpandedView'; import WebsiteHeader from './WebsiteHeader'; import WebsiteMetricsBar from './WebsiteMetricsBar'; import WebsiteTableView from './WebsiteTableView'; -import { FILTER_COLUMNS } from 'lib/constants'; +import { FILTER_COLUMNS } from '@/lib/constants'; export default function WebsiteDetailsPage({ websiteId }: { websiteId: string }) { const pathname = usePathname(); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx index 95e718b4..4858ec73 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx @@ -1,21 +1,22 @@ -import LinkButton from 'components/common/LinkButton'; -import { useLocale, useMessages, useNavigation } from 'components/hooks'; -import SideNav from 'components/layout/SideNav'; -import BrowsersTable from 'components/metrics/BrowsersTable'; -import CitiesTable from 'components/metrics/CitiesTable'; -import CountriesTable from 'components/metrics/CountriesTable'; -import DevicesTable from 'components/metrics/DevicesTable'; -import EventsTable from 'components/metrics/EventsTable'; -import HostsTable from 'components/metrics/HostsTable'; -import LanguagesTable from 'components/metrics/LanguagesTable'; -import OSTable from 'components/metrics/OSTable'; -import PagesTable from 'components/metrics/PagesTable'; -import QueryParametersTable from 'components/metrics/QueryParametersTable'; -import ReferrersTable from 'components/metrics/ReferrersTable'; -import RegionsTable from 'components/metrics/RegionsTable'; -import ScreenTable from 'components/metrics/ScreenTable'; -import TagsTable from 'components/metrics/TagsTable'; import { Dropdown, Icon, Icons, Item, Text } from 'react-basics'; +import LinkButton from '@/components/common/LinkButton'; +import { useLocale, useMessages, useNavigation } from '@/components/hooks'; +import SideNav from '@/components/layout/SideNav'; +import BrowsersTable from '@/components/metrics/BrowsersTable'; +import CitiesTable from '@/components/metrics/CitiesTable'; +import CountriesTable from '@/components/metrics/CountriesTable'; +import DevicesTable from '@/components/metrics/DevicesTable'; +import EventsTable from '@/components/metrics/EventsTable'; +import HostsTable from '@/components/metrics/HostsTable'; +import LanguagesTable from '@/components/metrics/LanguagesTable'; +import OSTable from '@/components/metrics/OSTable'; +import PagesTable from '@/components/metrics/PagesTable'; +import QueryParametersTable from '@/components/metrics/QueryParametersTable'; +import ReferrersTable from '@/components/metrics/ReferrersTable'; +import RegionsTable from '@/components/metrics/RegionsTable'; +import ScreenTable from '@/components/metrics/ScreenTable'; +import TagsTable from '@/components/metrics/TagsTable'; +import ChannelsTable from '@/components/metrics/ChannelsTable'; import styles from './WebsiteExpandedView.module.css'; const views = { @@ -24,6 +25,7 @@ const views = { exit: PagesTable, title: PagesTable, referrer: ReferrersTable, + grouped: ReferrersTable, host: HostsTable, browser: BrowsersTable, os: OSTable, @@ -36,6 +38,7 @@ const views = { event: EventsTable, query: QueryParametersTable, tag: TagsTable, + channel: ChannelsTable, }; export default function WebsiteExpandedView({ @@ -64,6 +67,11 @@ export default function WebsiteExpandedView({ label: formatMessage(labels.referrers), url: renderUrl({ view: 'referrer' }), }, + { + key: 'channel', + label: formatMessage(labels.channels), + url: renderUrl({ view: 'channel' }), + }, { key: 'browser', label: formatMessage(labels.browsers), diff --git a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx index a6229e95..02b74418 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx @@ -1,8 +1,8 @@ import { Button, Icon, Icons, Popup, PopupTrigger, Text } from 'react-basics'; -import PopupForm from 'app/(main)/reports/[reportId]/PopupForm'; -import FilterSelectForm from 'app/(main)/reports/[reportId]/FilterSelectForm'; -import { useFields, useMessages, useNavigation, useDateRange } from 'components/hooks'; -import { OPERATOR_PREFIXES } from 'lib/constants'; +import PopupForm from '@/app/(main)/reports/[reportId]/PopupForm'; +import FilterSelectForm from '@/app/(main)/reports/[reportId]/FilterSelectForm'; +import { useFields, useMessages, useNavigation, useDateRange } from '@/components/hooks'; +import { OPERATOR_PREFIXES } from '@/lib/constants'; import styles from './WebsiteFilterButton.module.css'; export function WebsiteFilterButton({ diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx index edd10b99..b568dd3d 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx @@ -1,13 +1,13 @@ import classNames from 'classnames'; -import Favicon from 'components/common/Favicon'; -import { useMessages, useTeamUrl, useWebsite } from 'components/hooks'; -import Icons from 'components/icons'; -import ActiveUsers from 'components/metrics/ActiveUsers'; +import Favicon from '@/components/common/Favicon'; +import { useMessages, useTeamUrl, useWebsite } from '@/components/hooks'; +import Icons from '@/components/icons'; +import ActiveUsers from '@/components/metrics/ActiveUsers'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { ReactNode } from 'react'; import { Button, Icon, Text } from 'react-basics'; -import Lightning from 'assets/lightning.svg'; +import Lightning from '@/assets/lightning.svg'; import styles from './WebsiteHeader.module.css'; export function WebsiteHeader({ diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index a6e7ad40..f206d3c9 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -1,14 +1,14 @@ -import classNames from 'classnames'; -import { useDateRange, useMessages, useSticky } from 'components/hooks'; -import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; -import MetricCard from 'components/metrics/MetricCard'; -import MetricsBar from 'components/metrics/MetricsBar'; -import { formatShortTime, formatLongNumber } from 'lib/format'; -import WebsiteFilterButton from './WebsiteFilterButton'; -import useWebsiteStats from 'components/hooks/queries/useWebsiteStats'; -import styles from './WebsiteMetricsBar.module.css'; import { Dropdown, Item } from 'react-basics'; -import useStore, { setWebsiteDateCompare } from 'store/websites'; +import classNames from 'classnames'; +import { useDateRange, useMessages, useSticky } from '@/components/hooks'; +import WebsiteDateFilter from '@/components/input/WebsiteDateFilter'; +import MetricCard from '@/components/metrics/MetricCard'; +import MetricsBar from '@/components/metrics/MetricsBar'; +import { formatShortTime, formatLongNumber } from '@/lib/format'; +import useWebsiteStats from '@/components/hooks/queries/useWebsiteStats'; +import useStore, { setWebsiteDateCompare } from '@/store/websites'; +import WebsiteFilterButton from './WebsiteFilterButton'; +import styles from './WebsiteMetricsBar.module.css'; export function WebsiteMetricsBar({ websiteId, diff --git a/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx b/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx index 3cdfdd5d..198ad030 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteProvider.tsx @@ -1,6 +1,6 @@ 'use client'; import { createContext, ReactNode, useEffect } from 'react'; -import { useModified, useWebsite } from 'components/hooks'; +import { useModified, useWebsite } from '@/components/hooks'; import { Loading } from 'react-basics'; export const WebsiteContext = createContext(null); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx index 2782cac6..02422075 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx @@ -1,13 +1,13 @@ -import { Grid, GridRow } from 'components/layout/Grid'; -import PagesTable from 'components/metrics/PagesTable'; -import ReferrersTable from 'components/metrics/ReferrersTable'; -import BrowsersTable from 'components/metrics/BrowsersTable'; -import OSTable from 'components/metrics/OSTable'; -import DevicesTable from 'components/metrics/DevicesTable'; -import WorldMap from 'components/metrics/WorldMap'; -import CountriesTable from 'components/metrics/CountriesTable'; -import EventsTable from 'components/metrics/EventsTable'; -import EventsChart from 'components/metrics/EventsChart'; +import { Grid, GridRow } from '@/components/layout/Grid'; +import PagesTable from '@/components/metrics/PagesTable'; +import ReferrersTable from '@/components/metrics/ReferrersTable'; +import BrowsersTable from '@/components/metrics/BrowsersTable'; +import OSTable from '@/components/metrics/OSTable'; +import DevicesTable from '@/components/metrics/DevicesTable'; +import WorldMap from '@/components/metrics/WorldMap'; +import CountriesTable from '@/components/metrics/CountriesTable'; +import EventsTable from '@/components/metrics/EventsTable'; +import EventsChart from '@/components/metrics/EventsChart'; import { usePathname } from 'next/navigation'; export default function WebsiteTableView({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx b/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx index 21cd6597..10a2eed1 100644 --- a/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx +++ b/src/app/(main)/websites/[websiteId]/compare/WebsiteComparePage.tsx @@ -1,9 +1,9 @@ 'use client'; import WebsiteHeader from '../WebsiteHeader'; import WebsiteMetricsBar from '../WebsiteMetricsBar'; -import FilterTags from 'components/metrics/FilterTags'; -import { useNavigation } from 'components/hooks'; -import { FILTER_COLUMNS } from 'lib/constants'; +import FilterTags from '@/components/metrics/FilterTags'; +import { useNavigation } from '@/components/hooks'; +import { FILTER_COLUMNS } from '@/lib/constants'; import WebsiteChart from '../WebsiteChart'; import WebsiteCompareTables from './WebsiteCompareTables'; diff --git a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx index af5a06d4..ce7f5b47 100644 --- a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx +++ b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx @@ -1,25 +1,25 @@ -import { useDateRange, useMessages, useNavigation } from 'components/hooks'; -import { Grid, GridRow } from 'components/layout/Grid'; -import SideNav from 'components/layout/SideNav'; -import BrowsersTable from 'components/metrics/BrowsersTable'; -import ChangeLabel from 'components/metrics/ChangeLabel'; -import CitiesTable from 'components/metrics/CitiesTable'; -import CountriesTable from 'components/metrics/CountriesTable'; -import DevicesTable from 'components/metrics/DevicesTable'; -import EventsTable from 'components/metrics/EventsTable'; -import LanguagesTable from 'components/metrics/LanguagesTable'; -import MetricsTable from 'components/metrics/MetricsTable'; -import OSTable from 'components/metrics/OSTable'; -import PagesTable from 'components/metrics/PagesTable'; -import QueryParametersTable from 'components/metrics/QueryParametersTable'; -import ReferrersTable from 'components/metrics/ReferrersTable'; -import RegionsTable from 'components/metrics/RegionsTable'; -import ScreenTable from 'components/metrics/ScreenTable'; -import TagsTable from 'components/metrics/TagsTable'; -import { getCompareDate } from 'lib/date'; -import { formatNumber } from 'lib/format'; +import { useDateRange, useMessages, useNavigation } from '@/components/hooks'; +import { Grid, GridRow } from '@/components/layout/Grid'; +import SideNav from '@/components/layout/SideNav'; +import BrowsersTable from '@/components/metrics/BrowsersTable'; +import ChangeLabel from '@/components/metrics/ChangeLabel'; +import CitiesTable from '@/components/metrics/CitiesTable'; +import CountriesTable from '@/components/metrics/CountriesTable'; +import DevicesTable from '@/components/metrics/DevicesTable'; +import EventsTable from '@/components/metrics/EventsTable'; +import LanguagesTable from '@/components/metrics/LanguagesTable'; +import MetricsTable from '@/components/metrics/MetricsTable'; +import OSTable from '@/components/metrics/OSTable'; +import PagesTable from '@/components/metrics/PagesTable'; +import QueryParametersTable from '@/components/metrics/QueryParametersTable'; +import ReferrersTable from '@/components/metrics/ReferrersTable'; +import RegionsTable from '@/components/metrics/RegionsTable'; +import ScreenTable from '@/components/metrics/ScreenTable'; +import TagsTable from '@/components/metrics/TagsTable'; +import { getCompareDate } from '@/lib/date'; +import { formatNumber } from '@/lib/format'; import { useState } from 'react'; -import useStore from 'store/websites'; +import useStore from '@/store/websites'; import styles from './WebsiteCompareTables.module.css'; const views = { diff --git a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx index 760f34f9..453aa9a8 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx @@ -1,9 +1,9 @@ import { GridColumn, GridTable } from 'react-basics'; -import { useEventDataProperties, useEventDataValues, useMessages } from 'components/hooks'; -import { LoadingPanel } from 'components/common/LoadingPanel'; -import PieChart from 'components/charts/PieChart'; +import { useEventDataProperties, useEventDataValues, useMessages } from '@/components/hooks'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import PieChart from '@/components/charts/PieChart'; import { useState } from 'react'; -import { CHART_COLORS } from 'lib/constants'; +import { CHART_COLORS } from '@/lib/constants'; import styles from './EventProperties.module.css'; export function EventProperties({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx index 32eb985c..ce9048d3 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsDataTable.tsx @@ -1,6 +1,6 @@ -import { useWebsiteEvents } from 'components/hooks'; +import { useWebsiteEvents } from '@/components/hooks'; import EventsTable from './EventsTable'; -import DataTable from 'components/common/DataTable'; +import DataTable from '@/components/common/DataTable'; import { ReactNode } from 'react'; export default function EventsDataTable({ diff --git a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx index d039b67f..e90a7790 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsMetricsBar.tsx @@ -1,9 +1,9 @@ -import { useMessages } from 'components/hooks'; -import useWebsiteSessionStats from 'components/hooks/queries/useWebsiteSessionStats'; -import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; -import MetricCard from 'components/metrics/MetricCard'; -import MetricsBar from 'components/metrics/MetricsBar'; -import { formatLongNumber } from 'lib/format'; +import { useMessages } from '@/components/hooks'; +import useWebsiteSessionStats from '@/components/hooks/queries/useWebsiteSessionStats'; +import WebsiteDateFilter from '@/components/input/WebsiteDateFilter'; +import MetricCard from '@/components/metrics/MetricCard'; +import MetricsBar from '@/components/metrics/MetricsBar'; +import { formatLongNumber } from '@/lib/format'; import { Flexbox } from 'react-basics'; export function EventsMetricsBar({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx index 7dfc0394..cf4c19ef 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx @@ -2,10 +2,10 @@ import WebsiteHeader from '../WebsiteHeader'; import EventsDataTable from './EventsDataTable'; import EventsMetricsBar from './EventsMetricsBar'; -import EventsChart from 'components/metrics/EventsChart'; -import { GridRow } from 'components/layout/Grid'; -import MetricsTable from 'components/metrics/MetricsTable'; -import { useMessages } from 'components/hooks'; +import EventsChart from '@/components/metrics/EventsChart'; +import { GridRow } from '@/components/layout/Grid'; +import MetricsTable from '@/components/metrics/MetricsTable'; +import { useMessages } from '@/components/hooks'; import { Item, Tabs } from 'react-basics'; import { useState } from 'react'; import EventProperties from './EventProperties'; diff --git a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx index 42eb8f7a..8e6cdf76 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx @@ -1,9 +1,9 @@ import { GridTable, GridColumn, Icon } from 'react-basics'; -import { useMessages, useTeamUrl, useTimezone } from 'components/hooks'; -import Empty from 'components/common/Empty'; -import Avatar from 'components/common/Avatar'; +import { useMessages, useTeamUrl, useTimezone } from '@/components/hooks'; +import Empty from '@/components/common/Empty'; +import Avatar from '@/components/common/Avatar'; import Link from 'next/link'; -import Icons from 'components/icons'; +import Icons from '@/components/icons'; export function EventsTable({ data = [] }) { const { formatTimezoneDate } = useTimezone(); diff --git a/src/app/(main)/websites/[websiteId]/layout.tsx b/src/app/(main)/websites/[websiteId]/layout.tsx index 1df69cd3..2542f65a 100644 --- a/src/app/(main)/websites/[websiteId]/layout.tsx +++ b/src/app/(main)/websites/[websiteId]/layout.tsx @@ -6,7 +6,7 @@ export default async function ({ params, }: { children: any; - params: { websiteId: string }; + params: Promise<{ websiteId: string }>; }) { const { websiteId } = await params; diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx index 51663441..c3a3b8f7 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeCountries.tsx @@ -1,9 +1,9 @@ import { useCallback } from 'react'; -import ListTable from 'components/metrics/ListTable'; -import { useLocale, useCountryNames, useMessages } from 'components/hooks'; +import ListTable from '@/components/metrics/ListTable'; +import { useLocale, useCountryNames, useMessages } from '@/components/hooks'; import classNames from 'classnames'; import styles from './RealtimeCountries.module.css'; -import TypeIcon from 'components/common/TypeIcon'; +import TypeIcon from '@/components/common/TypeIcon'; export function RealtimeCountries({ data }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx index c27143aa..6db56b76 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHeader.tsx @@ -1,6 +1,6 @@ -import MetricCard from 'components/metrics/MetricCard'; -import { useMessages } from 'components/hooks'; -import { RealtimeData } from 'lib/types'; +import MetricCard from '@/components/metrics/MetricCard'; +import { useMessages } from '@/components/hooks'; +import { RealtimeData } from '@/lib/types'; import styles from './RealtimeHeader.module.css'; export function RealtimeHeader({ data }: { data: RealtimeData }) { diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx index 0ed5fbde..104cf334 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeHome.tsx @@ -1,9 +1,9 @@ import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import { useApi, useMessages } from 'components/hooks'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; +import Page from '@/components/layout/Page'; +import PageHeader from '@/components/layout/PageHeader'; +import { useApi, useMessages } from '@/components/hooks'; +import EmptyPlaceholder from '@/components/common/EmptyPlaceholder'; export function RealtimeHome() { const { formatMessage, labels, messages } = useMessages(); diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx index f40be9db..bb0225cc 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx @@ -1,12 +1,11 @@ -import useFormat from 'components//hooks/useFormat'; -import Empty from 'components/common/Empty'; -import FilterButtons from 'components/common/FilterButtons'; -import { useCountryNames, useLocale, useMessages, useTimezone } from 'components/hooks'; -import Icons from 'components/icons'; -import { BROWSERS, OS_NAMES } from 'lib/constants'; -import { stringToColor } from 'lib/format'; -import { RealtimeData } from 'lib/types'; -import { safeDecodeURI } from 'next-basics'; +import useFormat from '@/components//hooks/useFormat'; +import Empty from '@/components/common/Empty'; +import FilterButtons from '@/components/common/FilterButtons'; +import { useCountryNames, useLocale, useMessages, useTimezone } from '@/components/hooks'; +import Icons from '@/components/icons'; +import { BROWSERS, OS_NAMES } from '@/lib/constants'; +import { stringToColor } from '@/lib/format'; +import { RealtimeData } from '@/lib/types'; import { useContext, useMemo, useState } from 'react'; import { Icon, SearchField, StatusLight, Text } from 'react-basics'; import { FixedSizeList } from 'react-window'; @@ -27,7 +26,7 @@ const icons = { export function RealtimeLog({ data }: { data: RealtimeData }) { const website = useContext(WebsiteContext); const [search, setSearch] = useState(''); - const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { formatMessage, labels, messages } = useMessages(); const { formatValue } = useFormat(); const { locale } = useLocale(); const { formatTimezoneDate } = useTimezone(); @@ -53,7 +52,7 @@ export function RealtimeLog({ data }: { data: RealtimeData }) { }, ]; - const getTime = ({ createdAt, firstAt }) => formatTimezoneDate(firstAt || createdAt, 'h:mm:ss'); + const getTime = ({ createdAt, firstAt }) => formatTimezoneDate(firstAt || createdAt, 'pp'); const getColor = ({ id, sessionId }) => stringToColor(sessionId || id); @@ -71,24 +70,19 @@ export function RealtimeLog({ data }: { data: RealtimeData }) { const { __type, eventName, urlPath: url, browser, os, country, device } = log; if (__type === TYPE_EVENT) { - return ( - {eventName || formatMessage(labels.unknown)}, - url: ( - - {url} - - ), - }} - /> - ); + return formatMessage(messages.eventLog, { + event: {eventName || formatMessage(labels.unknown)}, + url: ( + + {url} + + ), + }); } if (__type === TYPE_PAGEVIEW) { @@ -99,23 +93,18 @@ export function RealtimeLog({ data }: { data: RealtimeData }) { target="_blank" rel="noreferrer noopener" > - {safeDecodeURI(url)} + {url} ); } if (__type === TYPE_SESSION) { - return ( - {countryNames[country] || formatMessage(labels.unknown)}, - browser: {BROWSERS[browser]}, - os: {OS_NAMES[os] || os}, - device: {formatMessage(labels[device] || labels.unknown)}, - }} - /> - ); + return formatMessage(messages.visitorLog, { + country: {countryNames[country] || formatMessage(labels.unknown)}, + browser: {BROWSERS[browser]}, + os: {OS_NAMES[os] || os}, + device: {formatMessage(labels[device] || labels.unknown)}, + }); } }; diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx index 15b40f01..ce95bf41 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeUrls.tsx @@ -1,11 +1,11 @@ import { Key, useContext, useState } from 'react'; import { ButtonGroup, Button, Flexbox } from 'react-basics'; import thenby from 'thenby'; -import { percentFilter } from 'lib/filters'; -import ListTable from 'components/metrics/ListTable'; -import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants'; -import { useMessages } from 'components/hooks'; -import { RealtimeData } from 'lib/types'; +import { percentFilter } from '@/lib/filters'; +import ListTable from '@/components/metrics/ListTable'; +import { FILTER_PAGES, FILTER_REFERRERS } from '@/lib/constants'; +import { useMessages } from '@/components/hooks'; +import { RealtimeData } from '@/lib/types'; import { WebsiteContext } from '../WebsiteProvider'; export function RealtimeUrls({ data }: { data: RealtimeData }) { diff --git a/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx b/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx index 7030cc32..6edc28f9 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/WebsiteRealtimePage.tsx @@ -1,16 +1,16 @@ 'use client'; import { firstBy } from 'thenby'; -import { Grid, GridRow } from 'components/layout/Grid'; -import Page from 'components/layout/Page'; -import RealtimeChart from 'components/metrics/RealtimeChart'; -import WorldMap from 'components/metrics/WorldMap'; -import { useRealtime } from 'components/hooks'; +import { Grid, GridRow } from '@/components/layout/Grid'; +import Page from '@/components/layout/Page'; +import RealtimeChart from '@/components/metrics/RealtimeChart'; +import WorldMap from '@/components/metrics/WorldMap'; +import { useRealtime } from '@/components/hooks'; import RealtimeLog from './RealtimeLog'; import RealtimeHeader from './RealtimeHeader'; import RealtimeUrls from './RealtimeUrls'; import RealtimeCountries from './RealtimeCountries'; import WebsiteHeader from '../WebsiteHeader'; -import { percentFilter } from 'lib/filters'; +import { percentFilter } from '@/lib/filters'; export function WebsiteRealtimePage({ websiteId }) { const { data, isLoading, error } = useRealtime(websiteId); diff --git a/src/app/(main)/websites/[websiteId]/reports/WebsiteReportsPage.tsx b/src/app/(main)/websites/[websiteId]/reports/WebsiteReportsPage.tsx index 051f6ed3..e61aacb1 100644 --- a/src/app/(main)/websites/[websiteId]/reports/WebsiteReportsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/WebsiteReportsPage.tsx @@ -1,9 +1,9 @@ 'use client'; import Link from 'next/link'; import { Button, Flexbox, Icon, Icons, Text } from 'react-basics'; -import { useMessages, useTeamUrl } from 'components/hooks'; +import { useMessages, useTeamUrl } from '@/components/hooks'; import WebsiteHeader from '../WebsiteHeader'; -import ReportsDataTable from 'app/(main)/reports/ReportsDataTable'; +import ReportsDataTable from '@/app/(main)/reports/ReportsDataTable'; export function WebsiteReportsPage({ websiteId }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx index 49b63e74..a0b47bc9 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx @@ -1,9 +1,9 @@ import { GridColumn, GridTable } from 'react-basics'; -import { useSessionDataProperties, useSessionDataValues, useMessages } from 'components/hooks'; -import { LoadingPanel } from 'components/common/LoadingPanel'; -import PieChart from 'components/charts/PieChart'; +import { useSessionDataProperties, useSessionDataValues, useMessages } from '@/components/hooks'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import PieChart from '@/components/charts/PieChart'; import { useState } from 'react'; -import { CHART_COLORS } from 'lib/constants'; +import { CHART_COLORS } from '@/lib/constants'; import styles from './SessionProperties.module.css'; export function SessionProperties({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx index 788d0066..56e0df62 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx @@ -1,6 +1,6 @@ -import { useWebsiteSessions } from 'components/hooks'; +import { useWebsiteSessions } from '@/components/hooks'; import SessionsTable from './SessionsTable'; -import DataTable from 'components/common/DataTable'; +import DataTable from '@/components/common/DataTable'; import { ReactNode } from 'react'; export default function SessionsDataTable({ diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx index 803e7a06..62d60de8 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx @@ -1,9 +1,9 @@ -import { useMessages } from 'components/hooks'; -import useWebsiteSessionStats from 'components/hooks/queries/useWebsiteSessionStats'; -import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; -import MetricCard from 'components/metrics/MetricCard'; -import MetricsBar from 'components/metrics/MetricsBar'; -import { formatLongNumber } from 'lib/format'; +import { useMessages } from '@/components/hooks'; +import useWebsiteSessionStats from '@/components/hooks/queries/useWebsiteSessionStats'; +import WebsiteDateFilter from '@/components/input/WebsiteDateFilter'; +import MetricCard from '@/components/metrics/MetricCard'; +import MetricsBar from '@/components/metrics/MetricsBar'; +import { formatLongNumber } from '@/lib/format'; import { Flexbox } from 'react-basics'; export function SessionsMetricsBar({ websiteId }: { websiteId: string }) { diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx index 30fd193db..2ee044db 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx @@ -3,11 +3,11 @@ import WebsiteHeader from '../WebsiteHeader'; import SessionsDataTable from './SessionsDataTable'; import SessionsMetricsBar from './SessionsMetricsBar'; import SessionProperties from './SessionProperties'; -import WorldMap from 'components/metrics/WorldMap'; -import { GridRow } from 'components/layout/Grid'; +import WorldMap from '@/components/metrics/WorldMap'; +import { GridRow } from '@/components/layout/Grid'; import { Item, Tabs } from 'react-basics'; import { useState } from 'react'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import SessionsWeekly from './SessionsWeekly'; export function SessionsPage({ websiteId }) { diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx index 3fea4836..ddb3ed65 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx @@ -1,9 +1,9 @@ import Link from 'next/link'; import { GridColumn, GridTable } from 'react-basics'; -import { useFormat, useMessages, useTimezone } from 'components/hooks'; -import Avatar from 'components/common/Avatar'; +import { useFormat, useMessages, useTimezone } from '@/components/hooks'; +import Avatar from '@/components/common/Avatar'; import styles from './SessionsTable.module.css'; -import TypeIcon from 'components/common/TypeIcon'; +import TypeIcon from '@/components/common/TypeIcon'; export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean }) { const { formatTimezoneDate } = useTimezone(); diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx index 3e15ddfa..6663a563 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx @@ -1,7 +1,7 @@ import { format, startOfDay, addHours } from 'date-fns'; -import { useLocale, useMessages, useWebsiteSessionsWeekly } from 'components/hooks'; -import { LoadingPanel } from 'components/common/LoadingPanel'; -import { getDayOfWeekAsDate } from 'lib/date'; +import { useLocale, useMessages, useWebsiteSessionsWeekly } from '@/components/hooks'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { getDayOfWeekAsDate } from '@/lib/date'; import styles from './SessionsWeekly.module.css'; import classNames from 'classnames'; import { TooltipPopup } from 'react-basics'; @@ -10,6 +10,10 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) { const { data, ...props } = useWebsiteSessionsWeekly(websiteId); const { dateLocale } = useLocale(); const { labels, formatMessage } = useMessages(); + const { weekStartsOn } = dateLocale.options; + const daysOfWeek = Array(7) + .fill(weekStartsOn) + .map((d, i) => (d + i) % 7); const [, max] = data ? data.reduce((arr: number[], hours: number[], index: number) => { @@ -40,7 +44,9 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) { {Array(24) .fill(null) .map((_, i) => { - const label = format(addHours(startOfDay(new Date()), i), 'haaa'); + const label = format(addHours(startOfDay(new Date()), i), 'p', { locale: dateLocale }) + .replace(/\D00 ?/, '') + .toLowerCase(); return (
{label} @@ -48,33 +54,35 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) { ); })}
- {data?.map((day: number[], index: number) => { - return ( -
-
- {format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })} + {data && + daysOfWeek.map((index: number) => { + const day = data[index]; + return ( +
+
+ {format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })} +
+ {day?.map((hour: number) => { + const pct = hour / max; + return ( +
+ {hour > 0 && ( + +
+ + )} +
+ ); + })}
- {day?.map((hour: number) => { - const pct = hour / max; - return ( -
- {hour > 0 && ( - -
- - )} -
- ); - })} -
- ); - })} + ); + })}
); diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx index 642b93d9..a59d3a92 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx @@ -1,8 +1,9 @@ import { isSameDay } from 'date-fns'; import { Loading, Icon, StatusLight } from 'react-basics'; -import Icons from 'components/icons'; -import { useSessionActivity, useTimezone } from 'components/hooks'; +import Icons from '@/components/icons'; +import { useSessionActivity, useTimezone } from '@/components/hooks'; import styles from './SessionActivity.module.css'; +import { Fragment } from 'react'; export function SessionActivity({ websiteId, @@ -31,9 +32,9 @@ export function SessionActivity({ lastDay = createdAt; return ( - <> + {showHeader && ( -
{formatTimezoneDate(createdAt, 'EEEE, PPP')}
+
{formatTimezoneDate(createdAt, 'PPPP')}
)}
@@ -44,7 +45,7 @@ export function SessionActivity({ {eventName ? : }
{eventName || urlPath}
- + ); })}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx index 39b6afd1..56d4a0d9 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionData.tsx @@ -1,9 +1,9 @@ import { TextOverflow } from 'react-basics'; -import { useMessages, useSessionData } from 'components/hooks'; -import Empty from 'components/common/Empty'; -import { DATA_TYPES } from 'lib/constants'; +import { useMessages, useSessionData } from '@/components/hooks'; +import Empty from '@/components/common/Empty'; +import { DATA_TYPES } from '@/lib/constants'; import styles from './SessionData.module.css'; -import { LoadingPanel } from 'components/common/LoadingPanel'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; export function SessionData({ websiteId, sessionId }: { websiteId: string; sessionId: string }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx index d6a07edc..9ccf275f 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionDetailsPage.tsx @@ -1,7 +1,7 @@ 'use client'; -import Avatar from 'components/common/Avatar'; -import { LoadingPanel } from 'components/common/LoadingPanel'; -import { useWebsiteSession } from 'components/hooks'; +import Avatar from '@/components/common/Avatar'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useWebsiteSession } from '@/components/hooks'; import WebsiteHeader from '../../WebsiteHeader'; import { SessionActivity } from './SessionActivity'; import { SessionData } from './SessionData'; diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.tsx index 6f9a8f3d..3ce78d48 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionInfo.tsx @@ -1,7 +1,7 @@ -import { useFormat, useLocale, useMessages, useRegionNames, useTimezone } from 'components/hooks'; -import TypeIcon from 'components/common/TypeIcon'; +import { useFormat, useLocale, useMessages, useRegionNames, useTimezone } from '@/components/hooks'; +import TypeIcon from '@/components/common/TypeIcon'; import { Icon, CopyIcon } from 'react-basics'; -import Icons from 'components/icons'; +import Icons from '@/components/icons'; import styles from './SessionInfo.module.css'; export default function SessionInfo({ data }) { @@ -20,10 +20,10 @@ export default function SessionInfo({ data }) {
{formatMessage(labels.lastSeen)}
-
{formatTimezoneDate(data?.lastAt, 'EEEE, PPPpp')}
+
{formatTimezoneDate(data?.lastAt, 'PPPPpp')}
{formatMessage(labels.firstSeen)}
-
{formatTimezoneDate(data?.firstAt, 'EEEE, PPPpp')}
+
{formatTimezoneDate(data?.firstAt, 'PPPPpp')}
{formatMessage(labels.country)}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx index ea606582..eb385e9b 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionStats.tsx @@ -1,7 +1,7 @@ -import { useMessages } from 'components/hooks'; -import MetricCard from 'components/metrics/MetricCard'; -import MetricsBar from 'components/metrics/MetricsBar'; -import { formatShortTime } from 'lib/format'; +import { useMessages } from '@/components/hooks'; +import MetricCard from '@/components/metrics/MetricCard'; +import MetricsBar from '@/components/metrics/MetricsBar'; +import { formatShortTime } from '@/lib/format'; export function SessionStats({ data }) { const { formatMessage, labels } = useMessages(); diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx index bbc10a35..66884c2f 100644 --- a/src/app/Providers.tsx +++ b/src/app/Providers.tsx @@ -2,8 +2,8 @@ import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactBasicsProvider } from 'react-basics'; -import ErrorBoundary from 'components/common/ErrorBoundary'; -import { useLocale } from 'components/hooks'; +import ErrorBoundary from '@/components/common/ErrorBoundary'; +import { useLocale } from '@/components/hooks'; import 'chartjs-adapter-date-fns'; import { useEffect } from 'react'; diff --git a/src/app/actions/getConfig.ts b/src/app/actions/getConfig.ts new file mode 100644 index 00000000..bb892f01 --- /dev/null +++ b/src/app/actions/getConfig.ts @@ -0,0 +1,10 @@ +'use server'; + +export async function getConfig() { + return { + telemetryDisabled: !!process.env.DISABLE_TELEMETRY, + trackerScriptName: process.env.TRACKER_SCRIPT_NAME, + uiDisabled: !!process.env.DISABLE_UI, + updatesDisabled: !!process.env.DISABLE_UPDATES, + }; +} diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 00000000..2185e03e --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { pagingParams } from '@/lib/schema'; +import { canViewUsers } from '@/lib/auth'; +import { getUsers } from '@/queries/prisma/user'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canViewUsers(auth))) { + return unauthorized(); + } + + const users = await getUsers( + { + include: { + _count: { + select: { + websiteUser: { + where: { deletedAt: null }, + }, + }, + }, + }, + }, + query, + ); + + return json(users); +} diff --git a/src/app/api/admin/websites/route.ts b/src/app/api/admin/websites/route.ts new file mode 100644 index 00000000..3f35ea49 --- /dev/null +++ b/src/app/api/admin/websites/route.ts @@ -0,0 +1,90 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { pagingParams } from '@/lib/schema'; +import { canViewAllWebsites } from '@/lib/auth'; +import { getWebsites } from '@/queries/prisma/website'; +import { ROLES } from '@/lib/constants'; + +export async function GET(request: Request) { + const schema = z.object({ + userId: z.string().uuid(), + includeOwnedTeams: z.string().optional(), + includeAllTeams: z.string().optional(), + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canViewAllWebsites(auth))) { + return unauthorized(); + } + + const { userId, includeOwnedTeams, includeAllTeams } = query; + + const websites = await getWebsites( + { + where: { + OR: [ + ...(userId && [{ userId }]), + ...(userId && includeOwnedTeams + ? [ + { + team: { + deletedAt: null, + teamUser: { + some: { + role: ROLES.teamOwner, + userId, + }, + }, + }, + }, + ] + : []), + ...(userId && includeAllTeams + ? [ + { + team: { + deletedAt: null, + teamUser: { + some: { + userId, + }, + }, + }, + }, + ] + : []), + ], + }, + include: { + user: { + select: { + username: true, + id: true, + }, + }, + team: { + where: { + deletedAt: null, + }, + include: { + teamUser: { + where: { + role: ROLES.teamOwner, + }, + }, + }, + }, + }, + }, + query, + ); + + return json(websites); +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 00000000..7ae22e2b --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; +import { checkPassword } from '@/lib/auth'; +import { createSecureToken } from '@/lib/jwt'; +import redis from '@/lib/redis'; +import { getUserByUsername } from '@/queries'; +import { json, unauthorized } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { saveAuth } from '@/lib/auth'; +import { secret } from '@/lib/crypto'; +import { ROLES } from '@/lib/constants'; + +export async function POST(request: Request) { + const schema = z.object({ + username: z.string(), + password: z.string(), + }); + + const { body, error } = await parseRequest(request, schema, { skipAuth: true }); + + if (error) { + return error(); + } + + const { username, password } = body; + + const user = await getUserByUsername(username, { includePassword: true }); + + if (!user || !checkPassword(password, user.password)) { + return unauthorized(); + } + + const { id, role, createdAt } = user; + + let token: string; + + if (redis.enabled) { + token = await saveAuth({ userId: id, role }); + } else { + token = createSecureToken({ userId: user.id, role }, secret()); + } + + return json({ + token, + user: { id, username, role, createdAt, isAdmin: role === ROLES.admin }, + }); +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 00000000..7bf0a813 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,12 @@ +import redis from '@/lib/redis'; +import { ok } from '@/lib/response'; + +export async function POST(request: Request) { + if (redis.enabled) { + const token = request.headers.get('authorization')?.split(' ')?.[1]; + + await redis.client.del(token); + } + + return ok(); +} diff --git a/src/app/api/auth/sso/route.ts b/src/app/api/auth/sso/route.ts new file mode 100644 index 00000000..fc8fb9bf --- /dev/null +++ b/src/app/api/auth/sso/route.ts @@ -0,0 +1,18 @@ +import redis from '@/lib/redis'; +import { json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { saveAuth } from '@/lib/auth'; + +export async function POST(request: Request) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + if (redis.enabled) { + const token = await saveAuth({ userId: auth.user.id }, 86400); + + return json({ user: auth.user, token }); + } +} diff --git a/src/app/api/auth/verify/route.ts b/src/app/api/auth/verify/route.ts new file mode 100644 index 00000000..4d98b554 --- /dev/null +++ b/src/app/api/auth/verify/route.ts @@ -0,0 +1,12 @@ +import { parseRequest } from '@/lib/request'; +import { json } from '@/lib/response'; + +export async function GET(request: Request) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + return json(auth.user); +} diff --git a/src/app/api/heartbeat/route.ts b/src/app/api/heartbeat/route.ts new file mode 100644 index 00000000..91463089 --- /dev/null +++ b/src/app/api/heartbeat/route.ts @@ -0,0 +1,3 @@ +export async function GET() { + return Response.json({ ok: true }); +} diff --git a/src/app/api/me/password/route.ts b/src/app/api/me/password/route.ts new file mode 100644 index 00000000..69bef49b --- /dev/null +++ b/src/app/api/me/password/route.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { checkPassword, hashPassword } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { json, badRequest } from '@/lib/response'; +import { getUser, updateUser } from '@/queries/prisma/user'; + +export async function POST(request: Request) { + const schema = z.object({ + currentPassword: z.string(), + newPassword: z.string().min(8), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const userId = auth.user.id; + const { currentPassword, newPassword } = body; + + const user = await getUser(userId, { includePassword: true }); + + if (!checkPassword(currentPassword, user.password)) { + return badRequest('Current password is incorrect'); + } + + const password = hashPassword(newPassword); + + const updated = await updateUser(userId, { password }); + + return json(updated); +} diff --git a/src/app/api/me/route.ts b/src/app/api/me/route.ts new file mode 100644 index 00000000..59a32552 --- /dev/null +++ b/src/app/api/me/route.ts @@ -0,0 +1,12 @@ +import { parseRequest } from '@/lib/request'; +import { json } from '@/lib/response'; + +export async function GET(request: Request) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + return json(auth); +} diff --git a/src/app/api/me/teams/route.ts b/src/app/api/me/teams/route.ts new file mode 100644 index 00000000..2ea6575e --- /dev/null +++ b/src/app/api/me/teams/route.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +import { pagingParams } from '@/lib/schema'; +import { getUserTeams } from '@/queries'; +import { json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const teams = await getUserTeams(auth.user.id, query); + + return json(teams); +} diff --git a/src/app/api/me/websites/route.ts b/src/app/api/me/websites/route.ts new file mode 100644 index 00000000..a8df856a --- /dev/null +++ b/src/app/api/me/websites/route.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; +import { pagingParams } from '@/lib/schema'; +import { getUserWebsites } from '@/queries'; +import { json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const websites = await getUserWebsites(auth.user.id, query); + + return json(websites); +} diff --git a/src/app/api/realtime/[websiteId]/route.ts b/src/app/api/realtime/[websiteId]/route.ts new file mode 100644 index 00000000..7f9c1a9a --- /dev/null +++ b/src/app/api/realtime/[websiteId]/route.ts @@ -0,0 +1,30 @@ +import { json, unauthorized } from '@/lib/response'; +import { getRealtimeData } from '@/queries'; +import { canViewWebsite } from '@/lib/auth'; +import { startOfMinute, subMinutes } from 'date-fns'; +import { REALTIME_RANGE } from '@/lib/constants'; +import { parseRequest } from '@/lib/request'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, query, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { timezone } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); + + const data = await getRealtimeData(websiteId, { startDate, timezone }); + + return json(data); +} diff --git a/src/app/api/reports/[reportId]/route.ts b/src/app/api/reports/[reportId]/route.ts new file mode 100644 index 00000000..ba90ee08 --- /dev/null +++ b/src/app/api/reports/[reportId]/route.ts @@ -0,0 +1,91 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { deleteReport, getReport, updateReport } from '@/queries'; +import { canDeleteReport, canUpdateReport, canViewReport } from '@/lib/auth'; +import { unauthorized, json, notFound, ok } from '@/lib/response'; +import { reportTypeParam } from '@/lib/schema'; + +export async function GET(request: Request, { params }: { params: Promise<{ reportId: string }> }) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { reportId } = await params; + + const report = await getReport(reportId); + + if (!(await canViewReport(auth, report))) { + return unauthorized(); + } + + report.parameters = JSON.parse(report.parameters); + + return json(report); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ reportId: string }> }, +) { + const schema = z.object({ + websiteId: z.string().uuid(), + type: reportTypeParam, + name: z.string().max(200), + description: z.string().max(500), + parameters: z.object({}).passthrough(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { reportId } = await params; + const { websiteId, type, name, description, parameters } = body; + + const report = await getReport(reportId); + + if (!report) { + return notFound(); + } + + if (!(await canUpdateReport(auth, report))) { + return unauthorized(); + } + + const result = await updateReport(reportId, { + websiteId, + userId: auth.user.id, + type, + name, + description, + parameters: JSON.stringify(parameters), + } as any); + + return json(result); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ reportId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { reportId } = await params; + const report = await getReport(reportId); + + if (!(await canDeleteReport(auth, report))) { + return unauthorized(); + } + + await deleteReport(reportId); + + return ok(); +} diff --git a/src/app/api/reports/funnel/route.ts b/src/app/api/reports/funnel/route.ts new file mode 100644 index 00000000..6033c633 --- /dev/null +++ b/src/app/api/reports/funnel/route.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getFunnel } from '@/queries'; +import { reportParms } from '@/lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + window: z.coerce.number().positive(), + steps: z + .array( + z.object({ + type: z.string(), + value: z.string(), + }), + ) + .min(2), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + steps, + window, + dateRange: { startDate, endDate }, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getFunnel(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + steps, + windowMinutes: +window, + }); + + return json(data); +} diff --git a/src/app/api/reports/goals/route.ts b/src/app/api/reports/goals/route.ts new file mode 100644 index 00000000..5a2f6bd0 --- /dev/null +++ b/src/app/api/reports/goals/route.ts @@ -0,0 +1,57 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getGoals } from '@/queries/sql/reports/getGoals'; +import { reportParms } from '@/lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + goals: z + .array( + z + .object({ + type: z.string().regex(/url|event|event-data/), + value: z.string(), + goal: z.coerce.number(), + operator: z + .string() + .regex(/count|sum|average/) + .optional(), + property: z.string().optional(), + }) + .refine(data => { + if (data['type'] === 'event-data') { + return data['operator'] && data['property']; + } + return true; + }), + ) + .min(1), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + goals, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getGoals(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + goals, + }); + + return json(data); +} diff --git a/src/app/api/reports/insights/route.ts b/src/app/api/reports/insights/route.ts new file mode 100644 index 00000000..b3569cba --- /dev/null +++ b/src/app/api/reports/insights/route.ts @@ -0,0 +1,62 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getInsights } from '@/queries'; +import { reportParms } from '@/lib/schema'; + +function convertFilters(filters: any[]) { + return filters.reduce((obj, filter) => { + obj[filter.name] = filter; + + return obj; + }, {}); +} + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + fields: z + .array( + z.object({ + name: z.string(), + type: z.string(), + label: z.string(), + }), + ) + .min(1), + filters: z.array( + z.object({ + name: z.string(), + type: z.string(), + operator: z.string(), + value: z.string(), + }), + ), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + fields, + filters, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getInsights(websiteId, fields, { + ...convertFilters(filters), + startDate: new Date(startDate), + endDate: new Date(endDate), + }); + + return json(data); +} diff --git a/src/app/api/reports/journey/route.ts b/src/app/api/reports/journey/route.ts new file mode 100644 index 00000000..a1bc6290 --- /dev/null +++ b/src/app/api/reports/journey/route.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getJourney } from '@/queries'; +import { reportParms } from '@/lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + steps: z.coerce.number().min(3).max(7), + startStep: z.string(), + endStep: z.string(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + steps, + startStep, + endStep, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getJourney(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + steps, + startStep, + endStep, + }); + + return json(data); +} diff --git a/src/app/api/reports/retention/route.ts b/src/app/api/reports/retention/route.ts new file mode 100644 index 00000000..83220bb4 --- /dev/null +++ b/src/app/api/reports/retention/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getRetention } from '@/queries'; +import { reportParms, timezoneParam } from '@/lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + timezone: timezoneParam, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate }, + timezone, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getRetention(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + timezone, + }); + + return json(data); +} diff --git a/src/app/api/reports/revenue/route.ts b/src/app/api/reports/revenue/route.ts new file mode 100644 index 00000000..13a34f38 --- /dev/null +++ b/src/app/api/reports/revenue/route.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { reportParms, timezoneParam } from '@/lib/schema'; +import { getRevenue } from '@/queries/sql/reports/getRevenue'; +import { getRevenueValues } from '@/queries/sql/reports/getRevenueValues'; + +export async function GET(request: Request) { + const { auth, query, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId, startDate, endDate } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getRevenueValues(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + }); + + return json(data); +} + +export async function POST(request: Request) { + const schema = z.object({ + currency: z.string(), + ...reportParms, + timezone: timezoneParam, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + currency, + timezone, + dateRange: { startDate, endDate, unit }, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getRevenue(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + unit, + timezone, + currency, + }); + + return json(data); +} diff --git a/src/app/api/reports/route.ts b/src/app/api/reports/route.ts new file mode 100644 index 00000000..e50c57bc --- /dev/null +++ b/src/app/api/reports/route.ts @@ -0,0 +1,110 @@ +import { z } from 'zod'; +import { uuid } from '@/lib/crypto'; +import { pagingParams, reportTypeParam } from '@/lib/schema'; +import { parseRequest } from '@/lib/request'; +import { canViewTeam, canViewWebsite, canUpdateWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { getReports, createReport } from '@/queries'; + +export async function GET(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid().optional(), + teamId: z.string().uuid().optional(), + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { page, search, pageSize, websiteId, teamId } = query; + const userId = auth.user.id; + const filters = { + page, + pageSize, + search, + }; + + if ( + (websiteId && !(await canViewWebsite(auth, websiteId))) || + (teamId && !(await canViewTeam(auth, teamId))) + ) { + return unauthorized(); + } + + const data = await getReports( + { + where: { + OR: [ + ...(websiteId ? [{ websiteId }] : []), + ...(teamId + ? [ + { + website: { + deletedAt: null, + teamId, + }, + }, + ] + : []), + ...(userId && !websiteId && !teamId + ? [ + { + website: { + deletedAt: null, + userId, + }, + }, + ] + : []), + ], + }, + include: { + website: { + select: { + domain: true, + }, + }, + }, + }, + filters, + ); + + return json(data); +} + +export async function POST(request: Request) { + const schema = z.object({ + websiteId: z.string().uuid(), + name: z.string().max(200), + type: reportTypeParam, + description: z.string().max(500), + parameters: z.object({}).passthrough(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId, type, name, description, parameters } = body; + + if (!(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + const result = await createReport({ + id: uuid(), + userId: auth.user.id, + websiteId, + type, + name, + description, + parameters: JSON.stringify(parameters), + } as any); + + return json(result); +} diff --git a/src/app/api/reports/utm/route.ts b/src/app/api/reports/utm/route.ts new file mode 100644 index 00000000..38e88a6d --- /dev/null +++ b/src/app/api/reports/utm/route.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getUTM } from '@/queries'; +import { reportParms } from '@/lib/schema'; + +export async function POST(request: Request) { + const schema = z.object({ + ...reportParms, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { + websiteId, + dateRange: { startDate, endDate, timezone }, + } = body; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getUTM(websiteId, { + startDate: new Date(startDate), + endDate: new Date(endDate), + timezone, + }); + + return json(data); +} diff --git a/src/app/api/scripts/telemetry/route.ts b/src/app/api/scripts/telemetry/route.ts index ecd83fcb..54cee565 100644 --- a/src/app/api/scripts/telemetry/route.ts +++ b/src/app/api/scripts/telemetry/route.ts @@ -1,4 +1,4 @@ -import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants'; +import { CURRENT_VERSION, TELEMETRY_PIXEL } from '@/lib/constants'; export async function GET() { if ( diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts new file mode 100644 index 00000000..2d220f38 --- /dev/null +++ b/src/app/api/send/route.ts @@ -0,0 +1,203 @@ +import { z } from 'zod'; +import { isbot } from 'isbot'; +import { createToken, parseToken } from '@/lib/jwt'; +import clickhouse from '@/lib/clickhouse'; +import { parseRequest } from '@/lib/request'; +import { badRequest, json, forbidden, serverError } from '@/lib/response'; +import { fetchSession, fetchWebsite } from '@/lib/load'; +import { getClientInfo, hasBlockedIp } from '@/lib/detect'; +import { secret, uuid, visitSalt } from '@/lib/crypto'; +import { COLLECTION_TYPE, DOMAIN_REGEX } from '@/lib/constants'; +import { createSession, saveEvent, saveSessionData } from '@/queries'; +import { urlOrPathParam } from '@/lib/schema'; + +const schema = z.object({ + type: z.enum(['event', 'identify']), + payload: z.object({ + website: z.string().uuid(), + data: z.object({}).passthrough().optional(), + hostname: z.string().regex(DOMAIN_REGEX).max(100).optional(), + language: z.string().max(35).optional(), + referrer: urlOrPathParam.optional(), + screen: z.string().max(11).optional(), + title: z.string().optional(), + url: urlOrPathParam, + name: z.string().max(50).optional(), + tag: z.string().max(50).optional(), + ip: z.string().ip().optional(), + userAgent: z.string().optional(), + }), +}); + +export async function POST(request: Request) { + try { + // Bot check + if (!process.env.DISABLE_BOT_CHECK && isbot(request.headers.get('user-agent'))) { + return json({ beep: 'boop' }); + } + + const { body, error } = await parseRequest(request, schema, { skipAuth: true }); + + if (error) { + return error(); + } + + const { type, payload } = body; + + const { + website: websiteId, + hostname, + screen, + language, + url, + referrer, + name, + data, + title, + tag, + } = payload; + + // Cache check + let cache: { websiteId: string; sessionId: string; visitId: string; iat: number } | null = null; + const cacheHeader = request.headers.get('x-umami-cache'); + + if (cacheHeader) { + const result = await parseToken(cacheHeader, secret()); + + if (result) { + cache = result; + } + } + + // Find website + if (!cache?.websiteId) { + const website = await fetchWebsite(websiteId); + + if (!website) { + return badRequest('Website not found.'); + } + } + + // Client info + const { ip, userAgent, device, browser, os, country, subdivision1, subdivision2, city } = + await getClientInfo(request, payload); + + // IP block + if (hasBlockedIp(ip)) { + return forbidden(); + } + + const sessionId = uuid(websiteId, hostname, ip, userAgent); + + // Find session + if (!clickhouse.enabled && !cache?.sessionId) { + const session = await fetchSession(websiteId, sessionId); + + // Create a session if not found + if (!session) { + try { + await createSession({ + id: sessionId, + websiteId, + hostname, + browser, + os, + device, + screen, + language, + country, + subdivision1, + subdivision2, + city, + }); + } catch (e: any) { + if (!e.message.toLowerCase().includes('unique constraint')) { + return serverError(e); + } + } + } + } + + // Visit info + const now = Math.floor(new Date().getTime() / 1000); + let visitId = cache?.visitId || uuid(sessionId, visitSalt()); + let iat = cache?.iat || now; + + // Expire visit after 30 minutes + if (now - iat > 1800) { + visitId = uuid(sessionId, visitSalt()); + iat = now; + } + + if (type === COLLECTION_TYPE.event) { + const base = hostname ? `https://${hostname}` : 'https://localhost'; + const currentUrl = new URL(url, base); + + let urlPath = currentUrl.pathname; + const urlQuery = currentUrl.search.substring(1); + const urlDomain = currentUrl.hostname.replace(/^www./, ''); + + if (process.env.REMOVE_TRAILING_SLASH) { + urlPath = urlPath.replace(/(.+)\/$/, '$1'); + } + + let referrerPath: string; + let referrerQuery: string; + let referrerDomain: string; + + if (referrer) { + const referrerUrl = new URL(referrer, base); + + referrerPath = referrerUrl.pathname; + referrerQuery = referrerUrl.search.substring(1); + + if (referrerUrl.hostname !== 'localhost') { + referrerDomain = referrerUrl.hostname.replace(/^www\./, ''); + } + } + + await saveEvent({ + websiteId, + sessionId, + visitId, + urlPath, + urlQuery, + referrerPath, + referrerQuery, + referrerDomain, + pageTitle: title, + eventName: name, + eventData: data, + hostname: hostname || urlDomain, + browser, + os, + device, + screen, + language, + country, + subdivision1, + subdivision2, + city, + tag, + }); + } + + if (type === COLLECTION_TYPE.identify) { + if (!data) { + return badRequest('Data required.'); + } + + await saveSessionData({ + websiteId, + sessionId, + sessionData: data, + }); + } + + const token = createToken({ websiteId, sessionId, visitId, iat }, secret()); + + return json({ cache: token }); + } catch (e) { + return serverError(e); + } +} diff --git a/src/app/api/share/[shareId]/route.ts b/src/app/api/share/[shareId]/route.ts new file mode 100644 index 00000000..e387938d --- /dev/null +++ b/src/app/api/share/[shareId]/route.ts @@ -0,0 +1,19 @@ +import { json, notFound } from '@/lib/response'; +import { createToken } from '@/lib/jwt'; +import { secret } from '@/lib/crypto'; +import { getSharedWebsite } from '@/queries'; + +export async function GET(request: Request, { params }: { params: Promise<{ shareId: string }> }) { + const { shareId } = await params; + + const website = await getSharedWebsite(shareId); + + if (!website) { + return notFound(); + } + + const data = { websiteId: website.id }; + const token = createToken(data, secret()); + + return json({ ...data, token }); +} diff --git a/src/app/api/teams/[teamId]/route.ts b/src/app/api/teams/[teamId]/route.ts new file mode 100644 index 00000000..f7f4b331 --- /dev/null +++ b/src/app/api/teams/[teamId]/route.ts @@ -0,0 +1,71 @@ +import { z } from 'zod'; +import { unauthorized, json, notFound, ok } from '@/lib/response'; +import { canDeleteTeam, canUpdateTeam, canViewTeam } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { deleteTeam, getTeam, updateTeam } from '@/queries'; + +export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canViewTeam(auth, teamId))) { + return unauthorized(); + } + + const team = await getTeam(teamId, { includeMembers: true }); + + if (!team) { + return notFound('Team not found.'); + } + + return json(team); +} + +export async function POST(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ + name: z.string().max(50), + accessCode: z.string().max(50), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canUpdateTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const team = await updateTeam(teamId, body); + + return json(team); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ teamId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canDeleteTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + await deleteTeam(teamId); + + return ok(); +} diff --git a/src/app/api/teams/[teamId]/users/[userId]/route.ts b/src/app/api/teams/[teamId]/users/[userId]/route.ts new file mode 100644 index 00000000..bf5f4d36 --- /dev/null +++ b/src/app/api/teams/[teamId]/users/[userId]/route.ts @@ -0,0 +1,84 @@ +import { canDeleteTeamUser, canUpdateTeam } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { badRequest, json, ok, unauthorized } from '@/lib/response'; +import { deleteTeamUser, getTeamUser, updateTeamUser } from '@/queries'; +import { z } from 'zod'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ teamId: string; userId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { teamId, userId } = await params; + + if (!(await canUpdateTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const teamUser = await getTeamUser(teamId, userId); + + return json(teamUser); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ teamId: string; userId: string }> }, +) { + const schema = z.object({ + role: z.string().regex(/team-member|team-view-only|team-manager/), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { teamId, userId } = await params; + + if (!(await canUpdateTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const teamUser = await getTeamUser(teamId, userId); + + if (!teamUser) { + return badRequest('The User does not exists on this team.'); + } + + const user = await updateTeamUser(teamUser.id, body); + + return json(user); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ teamId: string; userId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { teamId, userId } = await params; + + if (!(await canDeleteTeamUser(auth, teamId, userId))) { + return unauthorized('You must be the owner of this team.'); + } + + const teamUser = await getTeamUser(teamId, userId); + + if (!teamUser) { + return badRequest('The User does not exists on this team.'); + } + + await deleteTeamUser(teamId, userId); + + return ok(); +} diff --git a/src/app/api/teams/[teamId]/users/route.ts b/src/app/api/teams/[teamId]/users/route.ts new file mode 100644 index 00000000..57460b89 --- /dev/null +++ b/src/app/api/teams/[teamId]/users/route.ts @@ -0,0 +1,77 @@ +import { z } from 'zod'; +import { unauthorized, json, badRequest } from '@/lib/response'; +import { canAddUserToTeam, canViewTeam } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { pagingParams, roleParam } from '@/lib/schema'; +import { createTeamUser, getTeamUser, getTeamUsers } from '@/queries'; + +export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canViewTeam(auth, teamId))) { + return unauthorized('You must be the owner of this team.'); + } + + const users = await getTeamUsers( + { + where: { + teamId, + user: { + deletedAt: null, + }, + }, + include: { + user: { + select: { + id: true, + username: true, + }, + }, + }, + }, + query, + ); + + return json(users); +} + +export async function POST(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ + userId: z.string().uuid(), + role: roleParam, + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { teamId } = await params; + + if (!(await canAddUserToTeam(auth))) { + return unauthorized(); + } + + const { userId, role } = body; + + const teamUser = await getTeamUser(teamId, userId); + + if (teamUser) { + return badRequest('User is already a member of the Team.'); + } + + const users = await createTeamUser(userId, teamId, role); + + return json(users); +} diff --git a/src/app/api/teams/[teamId]/websites/route.ts b/src/app/api/teams/[teamId]/websites/route.ts new file mode 100644 index 00000000..f69ab465 --- /dev/null +++ b/src/app/api/teams/[teamId]/websites/route.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; +import { unauthorized, json } from '@/lib/response'; +import { canViewTeam } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { pagingParams } from '@/lib/schema'; +import { getTeamWebsites } from '@/queries'; + +export async function GET(request: Request, { params }: { params: Promise<{ teamId: string }> }) { + const schema = z.object({ + ...pagingParams, + }); + const { teamId } = await params; + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canViewTeam(auth, teamId))) { + return unauthorized(); + } + + const websites = await getTeamWebsites(teamId, query); + + return json(websites); +} diff --git a/src/app/api/teams/join/route.ts b/src/app/api/teams/join/route.ts new file mode 100644 index 00000000..3464054c --- /dev/null +++ b/src/app/api/teams/join/route.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; +import { unauthorized, json, badRequest, notFound } from '@/lib/response'; +import { canCreateTeam } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { ROLES } from '@/lib/constants'; +import { createTeamUser, findTeam, getTeamUser } from '@/queries'; + +export async function POST(request: Request) { + const schema = z.object({ + accessCode: z.string().max(50), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canCreateTeam(auth))) { + return unauthorized(); + } + + const { accessCode } = body; + + const team = await findTeam({ + where: { + accessCode, + }, + }); + + if (!team) { + return notFound('Team not found.'); + } + + const teamUser = await getTeamUser(team.id, auth.user.id); + + if (teamUser) { + return badRequest('User is already a team member.'); + } + + const user = await createTeamUser(auth.user.id, team.id, ROLES.teamMember); + + return json(user); +} diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts new file mode 100644 index 00000000..d319d87b --- /dev/null +++ b/src/app/api/teams/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { getRandomChars } from '@/lib/crypto'; +import { unauthorized, json } from '@/lib/response'; +import { canCreateTeam } from '@/lib/auth'; +import { uuid } from '@/lib/crypto'; +import { parseRequest } from '@/lib/request'; +import { createTeam } from '@/queries'; + +export async function POST(request: Request) { + const schema = z.object({ + name: z.string().max(50), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canCreateTeam(auth))) { + return unauthorized(); + } + + const { name } = body; + + const team = await createTeam( + { + id: uuid(), + name, + accessCode: `team_${getRandomChars(16)}`, + }, + auth.user.id, + ); + + return json(team); +} diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts new file mode 100644 index 00000000..abb3331d --- /dev/null +++ b/src/app/api/users/[userId]/route.ts @@ -0,0 +1,101 @@ +import { z } from 'zod'; +import { canUpdateUser, canViewUser, canDeleteUser } from '@/lib/auth'; +import { getUser, getUserByUsername, updateUser, deleteUser } from '@/queries'; +import { json, unauthorized, badRequest, ok } from '@/lib/response'; +import { hashPassword } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { userId } = await params; + + if (!(await canViewUser(auth, userId))) { + return unauthorized(); + } + + const user = await getUser(userId); + + return json(user); +} + +export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ + username: z.string().max(255), + password: z.string().max(255), + role: z.string().regex(/admin|user|view-only/i), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { userId } = await params; + + if (!(await canUpdateUser(auth, userId))) { + return unauthorized(); + } + + const { username, password, role } = body; + + const user = await getUser(userId); + + const data: any = {}; + + if (password) { + data.password = hashPassword(password); + } + + // Only admin can change these fields + if (role && auth.user.isAdmin) { + data.role = role; + } + + if (username && auth.user.isAdmin) { + data.username = username; + } + + // Check when username changes + if (data.username && user.username !== data.username) { + const user = await getUserByUsername(username); + + if (user) { + return badRequest('User already exists'); + } + } + + const updated = await updateUser(userId, data); + + return json(updated); +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ userId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { userId } = await params; + + if (!(await canDeleteUser(auth))) { + return unauthorized(); + } + + if (userId === auth.user.id) { + return badRequest('You cannot delete yourself.'); + } + + await deleteUser(userId); + + return ok(); +} diff --git a/src/app/api/users/[userId]/teams/route.ts b/src/app/api/users/[userId]/teams/route.ts new file mode 100644 index 00000000..ff659525 --- /dev/null +++ b/src/app/api/users/[userId]/teams/route.ts @@ -0,0 +1,27 @@ +import { z } from 'zod'; +import { pagingParams } from '@/lib/schema'; +import { getUserTeams } from '@/queries'; +import { unauthorized, json } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { userId } = await params; + + if (auth.user.id !== userId && !auth.user.isAdmin) { + return unauthorized(); + } + + const teams = await getUserTeams(userId, query); + + return json(teams); +} diff --git a/src/app/api/users/[userId]/usage/route.ts b/src/app/api/users/[userId]/usage/route.ts new file mode 100644 index 00000000..e6ff217d --- /dev/null +++ b/src/app/api/users/[userId]/usage/route.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; +import { json, unauthorized } from '@/lib/response'; +import { getAllUserWebsitesIncludingTeamOwner } from '@/queries/prisma/website'; +import { getEventUsage } from '@/queries/sql/events/getEventUsage'; +import { getEventDataUsage } from '@/queries/sql/events/getEventDataUsage'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!auth.user.isAdmin) { + return unauthorized(); + } + + const { userId } = await params; + const { startAt, endAt } = query; + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const websites = await getAllUserWebsitesIncludingTeamOwner(userId); + + const websiteIds = websites.map(a => a.id); + + const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate); + const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate); + + const websiteUsage = websites.map(a => ({ + websiteId: a.id, + websiteName: a.name, + websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0, + eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0, + deletedAt: a.deletedAt, + })); + + const usage = websiteUsage.reduce( + (acc, cv) => { + acc.websiteEventUsage += cv.websiteEventUsage; + acc.eventDataUsage += cv.eventDataUsage; + + return acc; + }, + { websiteEventUsage: 0, eventDataUsage: 0 }, + ); + + const filteredWebsiteUsage = websiteUsage.filter( + a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0), + ); + + return json({ + ...usage, + websites: filteredWebsiteUsage, + }); +} diff --git a/src/app/api/users/[userId]/websites/route.ts b/src/app/api/users/[userId]/websites/route.ts new file mode 100644 index 00000000..77d41084 --- /dev/null +++ b/src/app/api/users/[userId]/websites/route.ts @@ -0,0 +1,27 @@ +import { z } from 'zod'; +import { unauthorized, json } from '@/lib/response'; +import { getUserWebsites } from '@/queries/prisma/website'; +import { pagingParams } from '@/lib/schema'; +import { parseRequest } from '@/lib/request'; + +export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { userId } = await params; + + if (!auth.user.isAdmin && auth.user.id !== userId) { + return unauthorized(); + } + + const websites = await getUserWebsites(userId, query); + + return json(websites); +} diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts new file mode 100644 index 00000000..320f72bd --- /dev/null +++ b/src/app/api/users/route.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { hashPassword, canCreateUser } from '@/lib/auth'; +import { ROLES } from '@/lib/constants'; +import { uuid } from '@/lib/crypto'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json, badRequest } from '@/lib/response'; +import { createUser, getUserByUsername } from '@/queries'; + +export async function POST(request: Request) { + const schema = z.object({ + username: z.string().max(255), + password: z.string(), + id: z.string().uuid(), + role: z.string().regex(/admin|user|view-only/i), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + if (!(await canCreateUser(auth))) { + return unauthorized(); + } + + const { username, password, role, id } = body; + + const existingUser = await getUserByUsername(username, { showDeleted: true }); + + if (existingUser) { + return badRequest('User already exists'); + } + + const user = await createUser({ + id: id || uuid(), + username, + password: hashPassword(password), + role: role ?? ROLES.user, + }); + + return json(user); +} diff --git a/src/app/api/version/route.ts b/src/app/api/version/route.ts new file mode 100644 index 00000000..275a4118 --- /dev/null +++ b/src/app/api/version/route.ts @@ -0,0 +1,6 @@ +import { json } from '@/lib/response'; +import { CURRENT_VERSION } from '@/lib/constants'; + +export async function GET() { + return json({ version: CURRENT_VERSION }); +} diff --git a/src/app/api/websites/[websiteId]/active/route.ts b/src/app/api/websites/[websiteId]/active/route.ts new file mode 100644 index 00000000..88c0fd17 --- /dev/null +++ b/src/app/api/websites/[websiteId]/active/route.ts @@ -0,0 +1,25 @@ +import { canViewWebsite } from '@/lib/auth'; +import { json, unauthorized } from '@/lib/response'; +import { getActiveVisitors } from '@/queries'; +import { parseRequest } from '@/lib/request'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const result = await getActiveVisitors(websiteId); + + return json(result); +} diff --git a/src/app/api/websites/[websiteId]/daterange/route.ts b/src/app/api/websites/[websiteId]/daterange/route.ts new file mode 100644 index 00000000..ea2d10d2 --- /dev/null +++ b/src/app/api/websites/[websiteId]/daterange/route.ts @@ -0,0 +1,25 @@ +import { canViewWebsite } from '@/lib/auth'; +import { getWebsiteDateRange } from '@/queries'; +import { json, unauthorized } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const result = await getWebsiteDateRange(websiteId); + + return json(result); +} diff --git a/src/app/api/websites/[websiteId]/event-data/events/route.ts b/src/app/api/websites/[websiteId]/event-data/events/route.ts new file mode 100644 index 00000000..aec7b471 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/events/route.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getEventDataEvents } from '@/queries/sql/events/getEventDataEvents'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + event: z.string().optional(), + }); + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt, event } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataEvents(websiteId, { + startDate, + endDate, + event, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/fields/route.ts b/src/app/api/websites/[websiteId]/event-data/fields/route.ts new file mode 100644 index 00000000..60101e45 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/fields/route.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getEventDataFields } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataFields(websiteId, { + startDate, + endDate, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/properties/route.ts b/src/app/api/websites/[websiteId]/event-data/properties/route.ts new file mode 100644 index 00000000..fe085f74 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/properties/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getEventDataProperties } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt, propertyName } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataProperties(websiteId, { startDate, endDate, propertyName }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/stats/route.ts b/src/app/api/websites/[websiteId]/event-data/stats/route.ts new file mode 100644 index 00000000..6928aa1e --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/stats/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getEventDataStats } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataStats(websiteId, { startDate, endDate }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts new file mode 100644 index 00000000..2a912439 --- /dev/null +++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getEventDataValues } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + eventName: z.string().optional(), + propertyName: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt, eventName, propertyName } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getEventDataValues(websiteId, { + startDate, + endDate, + eventName, + propertyName, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/events/route.ts b/src/app/api/websites/[websiteId]/events/route.ts new file mode 100644 index 00000000..66eaba2c --- /dev/null +++ b/src/app/api/websites/[websiteId]/events/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { pagingParams } from '@/lib/schema'; +import { getWebsiteEvents } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getWebsiteEvents(websiteId, { startDate, endDate }, query); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts new file mode 100644 index 00000000..da4b0d4f --- /dev/null +++ b/src/app/api/websites/[websiteId]/events/series/route.ts @@ -0,0 +1,45 @@ +import { z } from 'zod'; +import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; +import { getEventMetrics } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + unit: unitParam, + timezone: timezoneParam, + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { timezone } = query; + const { startDate, endDate, unit } = await getRequestDateRange(query); + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const filters = { + ...getRequestFilters(query), + startDate, + endDate, + timezone, + unit, + }; + + const data = await getEventMetrics(websiteId, filters); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts new file mode 100644 index 00000000..c0958739 --- /dev/null +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -0,0 +1,167 @@ +import { z } from 'zod'; +import thenby from 'thenby'; +import { canViewWebsite } from '@/lib/auth'; +import { + SESSION_COLUMNS, + EVENT_COLUMNS, + FILTER_COLUMNS, + OPERATORS, + SEARCH_DOMAINS, + SOCIAL_DOMAINS, + EMAIL_DOMAINS, + SHOPPING_DOMAINS, + VIDEO_DOMAINS, + PAID_AD_PARAMS, +} from '@/lib/constants'; +import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request'; +import { json, unauthorized, badRequest } from '@/lib/response'; +import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries'; +import { filterParams } from '@/lib/schema'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + type: z.string(), + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + limit: z.coerce.number().optional(), + offset: z.coerce.number().optional(), + search: z.string().optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { type, limit, offset, search } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate } = await getRequestDateRange(query); + const column = FILTER_COLUMNS[type] || type; + const filters = { + ...getRequestFilters(query), + startDate, + endDate, + }; + + if (search) { + filters[type] = { + name: type, + column, + operator: OPERATORS.contains, + value: search, + }; + } + + if (SESSION_COLUMNS.includes(type)) { + const data = await getSessionMetrics(websiteId, type, filters, limit, offset); + + if (type === 'language') { + const combined = {}; + + for (const { x, y } of data) { + const key = String(x).toLowerCase().split('-')[0]; + + if (combined[key] === undefined) { + combined[key] = { x: key, y }; + } else { + combined[key].y += y; + } + } + + return json(Object.values(combined)); + } + + return json(data); + } + + if (EVENT_COLUMNS.includes(type)) { + const data = await getPageviewMetrics(websiteId, type, filters, limit, offset); + + return json(data); + } + + if (type === 'channel') { + const data = await getChannelMetrics(websiteId, filters); + + const channels = getChannels(data); + + return json( + Object.keys(channels) + .map(key => ({ x: key, y: channels[key] })) + .sort(thenby.firstBy('y', -1)), + ); + } + + return badRequest(); +} + +function getChannels(data: { domain: string; query: string; visitors: number }[]) { + const channels = { + direct: 0, + referral: 0, + affiliate: 0, + email: 0, + sms: 0, + organicSearch: 0, + organicSocial: 0, + organicShopping: 0, + organicVideo: 0, + paidAds: 0, + paidSearch: 0, + paidSocial: 0, + paidShopping: 0, + paidVideo: 0, + }; + + const match = (value: string) => { + return (str: string | RegExp) => { + return typeof str === 'string' ? value.includes(str) : (str as RegExp).test(value); + }; + }; + + for (const { domain, query, visitors } of data) { + if (!domain && !query) { + channels.direct += Number(visitors); + } + + const prefix = /utm_medium=(.*cp.*|ppc|retargeting|paid.*)/.test(query) ? 'paid' : 'organic'; + + if (SEARCH_DOMAINS.some(match(domain)) || /utm_medium=organic/.test(query)) { + channels[`${prefix}Search`] += Number(visitors); + } else if ( + SOCIAL_DOMAINS.some(match(domain)) || + /utm_medium=(social|social-network|social-media|sm|social network|social media)/.test(query) + ) { + channels[`${prefix}Social`] += Number(visitors); + } else if (EMAIL_DOMAINS.some(match(domain)) || /utm_medium=(.*e[-_ ]?mail.*)/.test(query)) { + channels.email += Number(visitors); + } else if ( + SHOPPING_DOMAINS.some(match(domain)) || + /utm_campaign=(.*(([^a-df-z]|^)shop|shopping).*)/.test(query) + ) { + channels[`${prefix}Shopping`] += Number(visitors); + } else if (VIDEO_DOMAINS.some(match(domain)) || /utm_medium=(.*video.*)/.test(query)) { + channels[`${prefix}Video`] += Number(visitors); + } else if (PAID_AD_PARAMS.some(match(query))) { + channels.paidAds += Number(visitors); + } else if (/utm_medium=(referral|app|link)/.test(query)) { + channels.referral += Number(visitors); + } else if (/utm_medium=affiliate/.test(query)) { + channels.affiliate += Number(visitors); + } else if (/utm_(source|medium)=sms/.test(query)) { + channels.sms += Number(visitors); + } + } + + return channels; +} diff --git a/src/app/api/websites/[websiteId]/pageviews/route.ts b/src/app/api/websites/[websiteId]/pageviews/route.ts new file mode 100644 index 00000000..e603ae9c --- /dev/null +++ b/src/app/api/websites/[websiteId]/pageviews/route.ts @@ -0,0 +1,85 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request'; +import { unitParam, timezoneParam, filterParams } from '@/lib/schema'; +import { getCompareDate } from '@/lib/date'; +import { unauthorized, json } from '@/lib/response'; +import { getPageviewStats, getSessionStats } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + unit: unitParam, + timezone: timezoneParam, + compare: z.string().optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { timezone, compare } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate, unit } = await getRequestDateRange(query); + + const filters = { + ...getRequestFilters(query), + startDate, + endDate, + timezone, + unit, + }; + + const [pageviews, sessions] = await Promise.all([ + getPageviewStats(websiteId, filters), + getSessionStats(websiteId, filters), + ]); + + if (compare) { + const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( + compare, + startDate, + endDate, + ); + + const [comparePageviews, compareSessions] = await Promise.all([ + getPageviewStats(websiteId, { + ...filters, + startDate: compareStartDate, + endDate: compareEndDate, + }), + getSessionStats(websiteId, { + ...filters, + startDate: compareStartDate, + endDate: compareEndDate, + }), + ]); + + return json({ + pageviews, + sessions, + startDate, + endDate, + compare: { + pageviews: comparePageviews, + sessions: compareSessions, + startDate: compareStartDate, + endDate: compareEndDate, + }, + }); + } + + return json({ pageviews, sessions }); +} diff --git a/src/app/api/websites/[websiteId]/reports/route.ts b/src/app/api/websites/[websiteId]/reports/route.ts new file mode 100644 index 00000000..c6941f53 --- /dev/null +++ b/src/app/api/websites/[websiteId]/reports/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { getWebsiteReports } from '@/queries'; +import { pagingParams } from '@/lib/schema'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { page, pageSize, search } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getWebsiteReports(websiteId, { + page: +page, + pageSize: +pageSize, + search, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/reset/route.ts b/src/app/api/websites/[websiteId]/reset/route.ts new file mode 100644 index 00000000..62edceea --- /dev/null +++ b/src/app/api/websites/[websiteId]/reset/route.ts @@ -0,0 +1,25 @@ +import { canUpdateWebsite } from '@/lib/auth'; +import { resetWebsite } from '@/queries'; +import { unauthorized, ok } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + await resetWebsite(websiteId); + + return ok(); +} diff --git a/src/app/api/websites/[websiteId]/route.ts b/src/app/api/websites/[websiteId]/route.ts new file mode 100644 index 00000000..f4ea327b --- /dev/null +++ b/src/app/api/websites/[websiteId]/route.ts @@ -0,0 +1,84 @@ +import { z } from 'zod'; +import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from '@/lib/auth'; +import { SHARE_ID_REGEX } from '@/lib/constants'; +import { parseRequest } from '@/lib/request'; +import { ok, json, unauthorized, serverError } from '@/lib/response'; +import { deleteWebsite, getWebsite, updateWebsite } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const website = await getWebsite(websiteId); + + return json(website); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + name: z.string(), + domain: z.string(), + shareId: z.string().regex(SHARE_ID_REGEX).nullable(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { name, domain, shareId } = body; + + if (!(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + try { + const website = await updateWebsite(websiteId, { name, domain, shareId }); + + return Response.json(website); + } catch (e: any) { + if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { + return serverError(new Error('That share ID is already taken.')); + } + + return serverError(e); + } +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canDeleteWebsite(auth, websiteId))) { + return unauthorized(); + } + + await deleteWebsite(websiteId); + + return ok(); +} diff --git a/src/app/api/websites/[websiteId]/session-data/properties/route.ts b/src/app/api/websites/[websiteId]/session-data/properties/route.ts new file mode 100644 index 00000000..a6d9e2a4 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts @@ -0,0 +1,36 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getSessionDataProperties } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { startAt, endAt, propertyName } = query; + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getSessionDataProperties(websiteId, { startDate, endDate, propertyName }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/session-data/values/route.ts b/src/app/api/websites/[websiteId]/session-data/values/route.ts new file mode 100644 index 00000000..d950da34 --- /dev/null +++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts @@ -0,0 +1,40 @@ +import { canViewWebsite } from '@/lib/auth'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { getSessionDataValues } from '@/queries'; +import { z } from 'zod'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + propertyName: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { startAt, endAt, propertyName } = query; + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getSessionDataValues(websiteId, { + startDate, + endDate, + propertyName, + }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts new file mode 100644 index 00000000..aac40c38 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getSessionActivity } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string; sessionId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId, sessionId } = await params; + const { startAt, endAt } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getSessionActivity(websiteId, sessionId, startDate, endDate); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts new file mode 100644 index 00000000..9c389c82 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts @@ -0,0 +1,25 @@ +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getSessionData } from '@/queries'; +import { parseRequest } from '@/lib/request'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string; sessionId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId, sessionId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getSessionData(websiteId, sessionId); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts new file mode 100644 index 00000000..c4621ef4 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts @@ -0,0 +1,25 @@ +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getWebsiteSession } from '@/queries'; +import { parseRequest } from '@/lib/request'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string; sessionId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId, sessionId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getWebsiteSession(websiteId, sessionId); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/route.ts b/src/app/api/websites/[websiteId]/sessions/route.ts new file mode 100644 index 00000000..5a14f00f --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/route.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { pagingParams } from '@/lib/schema'; +import { getWebsiteSessions } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getWebsiteSessions(websiteId, { startDate, endDate }, query); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/stats/route.ts b/src/app/api/websites/[websiteId]/sessions/stats/route.ts new file mode 100644 index 00000000..e8e8e6c8 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; +import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { filterParams } from '@/lib/schema'; +import { getWebsiteSessionStats } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate } = await getRequestDateRange(query); + + const filters = getRequestFilters(query); + + const metrics = await getWebsiteSessionStats(websiteId, { + ...filters, + startDate, + endDate, + }); + + const data = Object.keys(metrics[0]).reduce((obj, key) => { + obj[key] = { + value: Number(metrics[0][key]) || 0, + }; + return obj; + }, {}); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts new file mode 100644 index 00000000..20be378d --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; +import { parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { pagingParams, timezoneParam } from '@/lib/schema'; +import { getWebsiteSessionsWeekly } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + timezone: timezoneParam, + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt, timezone } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate, timezone }); + + return json(data); +} diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts new file mode 100644 index 00000000..c146271f --- /dev/null +++ b/src/app/api/websites/[websiteId]/stats/route.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; +import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { getCompareDate } from '@/lib/date'; +import { filterParams } from '@/lib/schema'; +import { getWebsiteStats } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + compare: z.string().optional(), + ...filterParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { compare } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate } = await getRequestDateRange(query); + const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( + compare, + startDate, + endDate, + ); + + const filters = getRequestFilters(query); + + const metrics = await getWebsiteStats(websiteId, { + ...filters, + startDate, + endDate, + }); + + const prevPeriod = await getWebsiteStats(websiteId, { + ...filters, + startDate: compareStartDate, + endDate: compareEndDate, + }); + + const stats = Object.keys(metrics[0]).reduce((obj, key) => { + obj[key] = { + value: Number(metrics[0][key]) || 0, + prev: Number(prevPeriod[0][key]) || 0, + }; + return obj; + }, {}); + + return json(stats); +} diff --git a/src/app/api/websites/[websiteId]/transfer/route.ts b/src/app/api/websites/[websiteId]/transfer/route.ts new file mode 100644 index 00000000..03c0ae7f --- /dev/null +++ b/src/app/api/websites/[websiteId]/transfer/route.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from '@/lib/auth'; +import { updateWebsite } from '@/queries'; +import { parseRequest } from '@/lib/request'; +import { badRequest, unauthorized, json } from '@/lib/response'; + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + userId: z.string().uuid().optional(), + teamId: z.string().uuid().optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { userId, teamId } = body; + + if (userId) { + if (!(await canTransferWebsiteToUser(auth, websiteId, userId))) { + return unauthorized(); + } + + const website = await updateWebsite(websiteId, { + userId, + teamId: null, + }); + + return json(website); + } else if (teamId) { + if (!(await canTransferWebsiteToTeam(auth, websiteId, teamId))) { + return unauthorized(); + } + + const website = await updateWebsite(websiteId, { + userId: null, + teamId, + }); + + return json(website); + } + + return badRequest(); +} diff --git a/src/app/api/websites/[websiteId]/values/route.ts b/src/app/api/websites/[websiteId]/values/route.ts new file mode 100644 index 00000000..ed3cfae6 --- /dev/null +++ b/src/app/api/websites/[websiteId]/values/route.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { canViewWebsite } from '@/lib/auth'; +import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; +import { getValues } from '@/queries'; +import { parseRequest, getRequestDateRange } from '@/lib/request'; +import { badRequest, json, unauthorized } from '@/lib/response'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + type: z.string(), + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + search: z.string().optional(), + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { type, search } = query; + const { startDate, endDate } = await getRequestDateRange(query); + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type)) { + return badRequest('Invalid type.'); + } + + const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search); + + return json(values.filter(n => n).sort()); +} diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts new file mode 100644 index 00000000..b8fb2a0b --- /dev/null +++ b/src/app/api/websites/route.ts @@ -0,0 +1,59 @@ +import { z } from 'zod'; +import { canCreateTeamWebsite, canCreateWebsite } from '@/lib/auth'; +import { json, unauthorized } from '@/lib/response'; +import { uuid } from '@/lib/crypto'; +import { parseRequest } from '@/lib/request'; +import { createWebsite, getUserWebsites } from '@/queries'; +import { pagingParams } from '@/lib/schema'; + +export async function GET(request: Request) { + const schema = z.object({ ...pagingParams }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const websites = await getUserWebsites(auth.user.id, query); + + return json(websites); +} + +export async function POST(request: Request) { + const schema = z.object({ + name: z.string().max(100), + domain: z.string().max(500), + shareId: z.string().max(50).nullable().optional(), + teamId: z.string().nullable().optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { name, domain, shareId, teamId } = body; + + if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) { + return unauthorized(); + } + + const data: any = { + id: uuid(), + createdBy: auth.user.id, + name, + domain, + shareId, + teamId, + }; + + if (!teamId) { + data.userId = auth.user.id; + } + + const website = await createWebsite(data); + + return json(website); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3c0ed43c..f88d8169 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,8 +5,8 @@ import '@fontsource/inter/400.css'; import '@fontsource/inter/500.css'; import '@fontsource/inter/700.css'; import 'react-basics/dist/styles.css'; -import 'styles/index.css'; -import 'styles/variables.css'; +import '@/styles/index.css'; +import '@/styles/variables.css'; export default function ({ children }) { return ( diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx index 3101bf48..1da3106c 100644 --- a/src/app/login/LoginForm.tsx +++ b/src/app/login/LoginForm.tsx @@ -9,14 +9,14 @@ import { Icon, } from 'react-basics'; import { useRouter } from 'next/navigation'; -import { useApi, useMessages } from 'components/hooks'; -import { setUser } from 'store/app'; -import { setClientAuthToken } from 'lib/client'; -import Logo from 'assets/logo.svg'; +import { useApi, useMessages } from '@/components/hooks'; +import { setUser } from '@/store/app'; +import { setClientAuthToken } from '@/lib/client'; +import Logo from '@/assets/logo.svg'; import styles from './LoginForm.module.css'; export function LoginForm() { - const { formatMessage, labels, getMessage } = useMessages(); + const { formatMessage, labels } = useMessages(); const router = useRouter(); const { post, useMutation } = useApi(); const { mutate, error, isPending } = useMutation({ @@ -40,7 +40,7 @@ export function LoginForm() {
umami
- + }) { const { shareId } = await params; return ; diff --git a/src/app/sso/SSOPage.tsx b/src/app/sso/SSOPage.tsx index e577767a..eb7c0f0a 100644 --- a/src/app/sso/SSOPage.tsx +++ b/src/app/sso/SSOPage.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { Loading } from 'react-basics'; import { useRouter, useSearchParams } from 'next/navigation'; -import { setClientAuthToken } from 'lib/client'; +import { setClientAuthToken } from '@/lib/client'; export default function SSOPage() { const router = useRouter(); diff --git a/src/components/charts/BarChart.tsx b/src/components/charts/BarChart.tsx index 7c16730e..f6a6e5e0 100644 --- a/src/components/charts/BarChart.tsx +++ b/src/components/charts/BarChart.tsx @@ -1,7 +1,7 @@ -import BarChartTooltip from 'components/charts/BarChartTooltip'; -import Chart, { ChartProps } from 'components/charts/Chart'; -import { useTheme } from 'components/hooks'; -import { renderNumberLabels } from 'lib/charts'; +import BarChartTooltip from '@/components/charts/BarChartTooltip'; +import Chart, { ChartProps } from '@/components/charts/Chart'; +import { useTheme } from '@/components/hooks'; +import { renderNumberLabels } from '@/lib/charts'; import { useMemo, useState } from 'react'; export interface BarChartProps extends ChartProps { diff --git a/src/components/charts/BarChartTooltip.tsx b/src/components/charts/BarChartTooltip.tsx index 201c6e4c..af31c874 100644 --- a/src/components/charts/BarChartTooltip.tsx +++ b/src/components/charts/BarChartTooltip.tsx @@ -1,6 +1,6 @@ -import { useLocale } from 'components/hooks'; -import { formatDate } from 'lib/date'; -import { formatLongCurrency, formatLongNumber } from 'lib/format'; +import { useLocale } from '@/components/hooks'; +import { formatDate } from '@/lib/date'; +import { formatLongCurrency, formatLongNumber } from '@/lib/format'; import { Flexbox, StatusLight } from 'react-basics'; const formats = { diff --git a/src/components/charts/BubbleChart.tsx b/src/components/charts/BubbleChart.tsx index 956e260c..dfe67f3a 100644 --- a/src/components/charts/BubbleChart.tsx +++ b/src/components/charts/BubbleChart.tsx @@ -1,7 +1,7 @@ -import { Chart, ChartProps } from 'components/charts/Chart'; +import { Chart, ChartProps } from '@/components/charts/Chart'; import { useState } from 'react'; import { StatusLight } from 'react-basics'; -import { formatLongNumber } from 'lib/format'; +import { formatLongNumber } from '@/lib/format'; export interface BubbleChartProps extends ChartProps { type?: 'bubble'; diff --git a/src/components/charts/Chart.tsx b/src/components/charts/Chart.tsx index a4badbce..dde01eb4 100644 --- a/src/components/charts/Chart.tsx +++ b/src/components/charts/Chart.tsx @@ -2,9 +2,9 @@ import { useState, useRef, useEffect, useMemo, ReactNode } from 'react'; import { Loading } from 'react-basics'; import classNames from 'classnames'; import ChartJS, { LegendItem, ChartOptions } from 'chart.js/auto'; -import HoverTooltip from 'components/common/HoverTooltip'; -import Legend from 'components/metrics/Legend'; -import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; +import HoverTooltip from '@/components/common/HoverTooltip'; +import Legend from '@/components/metrics/Legend'; +import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants'; import styles from './Chart.module.css'; export interface ChartProps { diff --git a/src/components/charts/PieChart.tsx b/src/components/charts/PieChart.tsx index 57d676ca..a98b9730 100644 --- a/src/components/charts/PieChart.tsx +++ b/src/components/charts/PieChart.tsx @@ -1,7 +1,7 @@ -import { Chart, ChartProps } from 'components/charts/Chart'; +import { Chart, ChartProps } from '@/components/charts/Chart'; import { useState } from 'react'; import { StatusLight } from 'react-basics'; -import { formatLongNumber } from 'lib/format'; +import { formatLongNumber } from '@/lib/format'; export interface PieChartProps extends ChartProps { type?: 'doughnut' | 'pie'; diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 2e82b078..d0cae247 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { createAvatar } from '@dicebear/core'; import { lorelei } from '@dicebear/collection'; -import { getColor, getPastel } from 'lib/colors'; +import { getColor, getPastel } from '@/lib/colors'; const lib = lorelei; diff --git a/src/components/common/ConfirmationForm.tsx b/src/components/common/ConfirmationForm.tsx index 26b4ff24..8b617ab5 100644 --- a/src/components/common/ConfirmationForm.tsx +++ b/src/components/common/ConfirmationForm.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; import { Button, LoadingButton, Form, FormButtons } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export interface ConfirmationFormProps { message: ReactNode; diff --git a/src/components/common/DataTable.tsx b/src/components/common/DataTable.tsx index d2094329..b19ddf91 100644 --- a/src/components/common/DataTable.tsx +++ b/src/components/common/DataTable.tsx @@ -1,12 +1,12 @@ import { ReactNode } from 'react'; import classNames from 'classnames'; import { Loading, SearchField } from 'react-basics'; -import { useMessages, useNavigation } from 'components/hooks'; -import Empty from 'components/common/Empty'; -import Pager from 'components/common/Pager'; -import { PagedQueryResult } from 'lib/types'; +import { useMessages, useNavigation } from '@/components/hooks'; +import Empty from '@/components/common/Empty'; +import Pager from '@/components/common/Pager'; +import { PagedQueryResult } from '@/lib/types'; import styles from './DataTable.module.css'; -import { LoadingPanel } from 'components/common/LoadingPanel'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; const DEFAULT_SEARCH_DELAY = 600; @@ -37,26 +37,26 @@ export function DataTable({ query: { error, isLoading, isFetched }, } = queryResult || {}; const { page, pageSize, count, data } = result || {}; - const { query } = params || {}; + const { search } = params || {}; const hasData = Boolean(!isLoading && data?.length); - const noResults = Boolean(query && !hasData); + const noResults = Boolean(search && !hasData); const { router, renderUrl } = useNavigation(); - const handleSearch = (query: string) => { - setParams({ ...params, query, page: params.page ? page : 1 }); + const handleSearch = (search: string) => { + setParams({ ...params, search, page: params.page ? page : 1 }); }; const handlePageChange = (page: number) => { - setParams({ ...params, query, page }); + setParams({ ...params, search, page }); router.push(renderUrl({ page })); }; return ( <> - {allowSearch && (hasData || query) && ( + {allowSearch && (hasData || search) && ( {hasData ? (typeof children === 'function' ? children(result) : children) : null} {isLoading && } - {!isLoading && !hasData && !query && (renderEmpty ? renderEmpty() : )} + {!isLoading && !hasData && !search && (renderEmpty ? renderEmpty() : )} {!isLoading && noResults && }
{allowPaging && hasData && ( diff --git a/src/components/common/Empty.tsx b/src/components/common/Empty.tsx index 8e7d2d00..cf6d11cc 100644 --- a/src/components/common/Empty.tsx +++ b/src/components/common/Empty.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './Empty.module.css'; export interface EmptyProps { diff --git a/src/components/common/EmptyPlaceholder.tsx b/src/components/common/EmptyPlaceholder.tsx index 640e45d5..2fd606cd 100644 --- a/src/components/common/EmptyPlaceholder.tsx +++ b/src/components/common/EmptyPlaceholder.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; import { Icon, Text, Flexbox } from 'react-basics'; -import Logo from 'assets/logo.svg'; +import Logo from '@/assets/logo.svg'; export interface EmptyPlaceholderProps { message?: string; diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx index 9669580f..b9521bb4 100644 --- a/src/components/common/ErrorBoundary.tsx +++ b/src/components/common/ErrorBoundary.tsx @@ -1,7 +1,7 @@ import { ErrorInfo, ReactNode } from 'react'; import { ErrorBoundary as Boundary } from 'react-error-boundary'; import { Button } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './ErrorBoundary.module.css'; const logError = (error: Error, info: ErrorInfo) => { diff --git a/src/components/common/ErrorMessage.tsx b/src/components/common/ErrorMessage.tsx index 7ed8662a..bf3eefb1 100644 --- a/src/components/common/ErrorMessage.tsx +++ b/src/components/common/ErrorMessage.tsx @@ -1,6 +1,6 @@ import { Icon, Icons, Text } from 'react-basics'; import styles from './ErrorMessage.module.css'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export function ErrorMessage() { const { formatMessage, messages } = useMessages(); diff --git a/src/components/common/Favicon.tsx b/src/components/common/Favicon.tsx index 47c65aab..ea3f31aa 100644 --- a/src/components/common/Favicon.tsx +++ b/src/components/common/Favicon.tsx @@ -1,3 +1,5 @@ +import { GROUPED_DOMAINS } from '@/lib/constants'; + function getHostName(url: string) { const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?([^:/\n?=]+)/im); return match && match.length > 1 ? match[1] : null; @@ -9,16 +11,11 @@ export function Favicon({ domain, ...props }) { } const hostName = domain ? getHostName(domain) : null; + const src = hostName + ? `https://icons.duckduckgo.com/ip3/${GROUPED_DOMAINS[hostName]?.domain || hostName}.ico` + : null; - return hostName ? ( - - ) : null; + return hostName ? : null; } export default Favicon; diff --git a/src/components/common/FilterLink.tsx b/src/components/common/FilterLink.tsx index ef278ed2..9d726b58 100644 --- a/src/components/common/FilterLink.tsx +++ b/src/components/common/FilterLink.tsx @@ -1,6 +1,5 @@ import classNames from 'classnames'; -import { useMessages, useNavigation } from 'components/hooks'; -import { safeDecodeURIComponent } from 'next-basics'; +import { useMessages, useNavigation } from '@/components/hooks'; import Link from 'next/link'; import { ReactNode } from 'react'; import { Icon, Icons } from 'react-basics'; @@ -39,7 +38,7 @@ export function FilterLink({ {!value && `(${label || formatMessage(labels.unknown)})`} {value && ( - {safeDecodeURIComponent(label || value)} + {label || value} )} {externalUrl && ( diff --git a/src/components/common/LinkButton.tsx b/src/components/common/LinkButton.tsx index 83d95151..3aa2a76a 100644 --- a/src/components/common/LinkButton.tsx +++ b/src/components/common/LinkButton.tsx @@ -1,8 +1,8 @@ +import { ReactNode } from 'react'; import classNames from 'classnames'; import Link from 'next/link'; -import { useLocale } from 'components/hooks'; +import { useLocale } from '@/components/hooks'; import styles from './LinkButton.module.css'; -import { ReactNode } from 'react'; export interface LinkButtonProps { href: string; diff --git a/src/components/common/LoadingPanel.tsx b/src/components/common/LoadingPanel.tsx index 36de9365..4d27618a 100644 --- a/src/components/common/LoadingPanel.tsx +++ b/src/components/common/LoadingPanel.tsx @@ -1,8 +1,8 @@ import { ReactNode } from 'react'; import classNames from 'classnames'; import { Loading } from 'react-basics'; -import ErrorMessage from 'components/common/ErrorMessage'; -import Empty from 'components/common/Empty'; +import ErrorMessage from '@/components/common/ErrorMessage'; +import Empty from '@/components/common/Empty'; import styles from './LoadingPanel.module.css'; export function LoadingPanel({ diff --git a/src/components/common/Pager.tsx b/src/components/common/Pager.tsx index 3e0a8033..b33d2236 100644 --- a/src/components/common/Pager.tsx +++ b/src/components/common/Pager.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { Button, Icon, Icons } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './Pager.module.css'; export interface PagerProps { diff --git a/src/components/common/TypeConfirmationForm.tsx b/src/components/common/TypeConfirmationForm.tsx index 2dfb2dff..baf5949f 100644 --- a/src/components/common/TypeConfirmationForm.tsx +++ b/src/components/common/TypeConfirmationForm.tsx @@ -7,7 +7,7 @@ import { TextField, SubmitButton, } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export function TypeConfirmationForm({ confirmationValue, @@ -26,7 +26,7 @@ export function TypeConfirmationForm({ onConfirm?: () => void; onClose?: () => void; }) { - const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { formatMessage, labels, messages } = useMessages(); if (!confirmationValue) { return null; @@ -35,10 +35,7 @@ export function TypeConfirmationForm({ return (

- {confirmationValue} }} - /> + {formatMessage(messages.actionConfirmation, { confirmation: {confirmationValue} })}

value === confirmationValue }}> diff --git a/src/components/hooks/queries/useConfig.ts b/src/components/hooks/queries/useConfig.ts index f6293a44..223f4550 100644 --- a/src/components/hooks/queries/useConfig.ts +++ b/src/components/hooks/queries/useConfig.ts @@ -1,23 +1,16 @@ import { useEffect } from 'react'; -import useStore, { setConfig } from 'store/app'; -import { useApi } from '../useApi'; - -let loading = false; +import useStore, { setConfig } from '@/store/app'; +import { getConfig } from '@/app/actions/getConfig'; export function useConfig() { const { config } = useStore(); - const { get } = useApi(); - const configUrl = process.env.configUrl; async function loadConfig() { - const data = await get(configUrl); - loading = false; - setConfig(data); + setConfig(await getConfig()); } useEffect(() => { - if (!config && !loading && configUrl) { - loading = true; + if (!config) { loadConfig(); } }, []); diff --git a/src/components/hooks/queries/useLogin.ts b/src/components/hooks/queries/useLogin.ts index a54f38d1..f88efbf0 100644 --- a/src/components/hooks/queries/useLogin.ts +++ b/src/components/hooks/queries/useLogin.ts @@ -1,5 +1,5 @@ import { UseQueryResult } from '@tanstack/react-query'; -import useStore, { setUser } from 'store/app'; +import useStore, { setUser } from '@/store/app'; import { useApi } from '../useApi'; const selector = (state: { user: any }) => state.user; diff --git a/src/components/hooks/queries/useRealtime.ts b/src/components/hooks/queries/useRealtime.ts index b87f74c4..670b23be 100644 --- a/src/components/hooks/queries/useRealtime.ts +++ b/src/components/hooks/queries/useRealtime.ts @@ -1,6 +1,6 @@ -import { useTimezone } from 'components/hooks'; -import { REALTIME_INTERVAL } from 'lib/constants'; -import { RealtimeData } from 'lib/types'; +import { useTimezone } from '@/components/hooks/useTimezone'; +import { REALTIME_INTERVAL } from '@/lib/constants'; +import { RealtimeData } from '@/lib/types'; import { useApi } from '../useApi'; export function useRealtime(websiteId: string) { diff --git a/src/components/hooks/queries/useReport.ts b/src/components/hooks/queries/useReport.ts index f7d2a1a0..45aea19c 100644 --- a/src/components/hooks/queries/useReport.ts +++ b/src/components/hooks/queries/useReport.ts @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useApi } from '../useApi'; import { useTimezone } from '../useTimezone'; import { useMessages } from '../useMessages'; +import { parseDateRange } from '@/lib/date'; export function useReport( reportId: string, @@ -24,14 +25,12 @@ export function useReport( const data: any = await get(`/reports/${id}`); const { dateRange } = data?.parameters || {}; - const { startDate, endDate } = dateRange || {}; - if (startDate && endDate) { - dateRange.startDate = new Date(startDate); - dateRange.endDate = new Date(endDate); - } - - data.parameters = { ...defaultParameters?.parameters, ...data.parameters }; + data.parameters = { + ...defaultParameters?.parameters, + ...data.parameters, + dateRange: parseDateRange(dateRange.value), + }; setReport(data); }; diff --git a/src/components/hooks/queries/useSessionDataProperties.ts b/src/components/hooks/queries/useSessionDataProperties.ts index 45590b39..ca3798f0 100644 --- a/src/components/hooks/queries/useSessionDataProperties.ts +++ b/src/components/hooks/queries/useSessionDataProperties.ts @@ -10,7 +10,7 @@ export function useSessionDataProperties( const params = useFilterParams(websiteId); return useQuery({ - queryKey: ['websites:event-data:properties', { websiteId, ...params }], + queryKey: ['websites:session-data:properties', { websiteId, ...params }], queryFn: () => get(`/websites/${websiteId}/session-data/properties`, { ...params }), enabled: !!websiteId, ...options, diff --git a/src/components/hooks/queries/useShareToken.ts b/src/components/hooks/queries/useShareToken.ts index f9db7dbf..cf17c756 100644 --- a/src/components/hooks/queries/useShareToken.ts +++ b/src/components/hooks/queries/useShareToken.ts @@ -1,4 +1,4 @@ -import useStore, { setShareToken } from 'store/app'; +import useStore, { setShareToken } from '@/store/app'; import { useApi } from '../useApi'; const selector = (state: { shareToken: string }) => state.shareToken; diff --git a/src/components/hooks/queries/useTeams.ts b/src/components/hooks/queries/useTeams.ts index e5197c97..d09e2f7d 100644 --- a/src/components/hooks/queries/useTeams.ts +++ b/src/components/hooks/queries/useTeams.ts @@ -11,6 +11,7 @@ export function useTeams(userId: string) { queryFn: (params: any) => { return get(`/users/${userId}/teams`, params); }, + enabled: !!userId, }); } diff --git a/src/components/hooks/queries/useWebsitePageviews.ts b/src/components/hooks/queries/useWebsitePageviews.ts index 42fb527e..43c51745 100644 --- a/src/components/hooks/queries/useWebsitePageviews.ts +++ b/src/components/hooks/queries/useWebsitePageviews.ts @@ -1,6 +1,6 @@ import { UseQueryOptions } from '@tanstack/react-query'; import { useApi } from '../useApi'; -import { useFilterParams } from '..//useFilterParams'; +import { useFilterParams } from '../useFilterParams'; export function useWebsitePageviews( websiteId: string, diff --git a/src/components/hooks/queries/useWebsiteSessions.ts b/src/components/hooks/queries/useWebsiteSessions.ts index ad7bb616..09e34a80 100644 --- a/src/components/hooks/queries/useWebsiteSessions.ts +++ b/src/components/hooks/queries/useWebsiteSessions.ts @@ -1,7 +1,7 @@ import { useApi } from '../useApi'; import { usePagedQuery } from '../usePagedQuery'; import useModified from '../useModified'; -import { useFilterParams } from 'components/hooks/useFilterParams'; +import { useFilterParams } from '@/components/hooks/useFilterParams'; export function useWebsiteSessions(websiteId: string, params?: { [key: string]: string | number }) { const { get } = useApi(); diff --git a/src/components/hooks/queries/useWebsiteSessionsWeekly.ts b/src/components/hooks/queries/useWebsiteSessionsWeekly.ts index c4e83f98..f3aa3b00 100644 --- a/src/components/hooks/queries/useWebsiteSessionsWeekly.ts +++ b/src/components/hooks/queries/useWebsiteSessionsWeekly.ts @@ -1,6 +1,6 @@ import { useApi } from '../useApi'; import useModified from '../useModified'; -import { useFilterParams } from 'components/hooks/useFilterParams'; +import { useFilterParams } from '@/components/hooks/useFilterParams'; export function useWebsiteSessionsWeekly( websiteId: string, diff --git a/src/components/hooks/queries/useWebsiteValues.ts b/src/components/hooks/queries/useWebsiteValues.ts index 73a7c755..77f65fe5 100644 --- a/src/components/hooks/queries/useWebsiteValues.ts +++ b/src/components/hooks/queries/useWebsiteValues.ts @@ -1,5 +1,6 @@ import { useApi } from '../useApi'; -import { useCountryNames, useRegionNames } from 'components/hooks'; +import { useCountryNames } from '@/components/hooks/useCountryNames'; +import { useRegionNames } from '@/components/hooks/useRegionNames'; import useLocale from '../useLocale'; export function useWebsiteValues({ diff --git a/src/components/hooks/useApi.ts b/src/components/hooks/useApi.ts index e806d37e..dfa48e2f 100644 --- a/src/components/hooks/useApi.ts +++ b/src/components/hooks/useApi.ts @@ -1,20 +1,78 @@ +import { useCallback } from 'react'; import * as reactQuery from '@tanstack/react-query'; -import { useApi as nextUseApi } from 'next-basics'; -import { getClientAuthToken } from 'lib/client'; -import { SHARE_TOKEN_HEADER } from 'lib/constants'; -import useStore from 'store/app'; +import { getClientAuthToken } from '@/lib/client'; +import { SHARE_TOKEN_HEADER } from '@/lib/constants'; +import { httpGet, httpPost, httpPut, httpDelete, FetchResponse } from '@/lib/fetch'; +import useStore from '@/store/app'; const selector = (state: { shareToken: { token?: string } }) => state.shareToken; +async function handleResponse(res: FetchResponse): Promise { + if (!res.ok) { + return Promise.reject(new Error(res.error?.error || res.error || 'Unexpectd error.')); + } + return Promise.resolve(res.data); +} + +function handleError(err: Error | string) { + return Promise.reject((err as Error)?.message || err || null); +} + export function useApi() { const shareToken = useStore(selector); - const { get, post, put, del } = nextUseApi( - { authorization: `Bearer ${getClientAuthToken()}`, [SHARE_TOKEN_HEADER]: shareToken?.token }, - process.env.basePath, - ); + const defaultHeaders = { + authorization: `Bearer ${getClientAuthToken()}`, + [SHARE_TOKEN_HEADER]: shareToken?.token, + }; + const basePath = process.env.basePath; - return { get, post, put, del, ...reactQuery }; + const getUrl = (url: string) => { + return url.startsWith('http') ? url : `${basePath || ''}/api${url}`; + }; + + const getHeaders = (headers: any = {}) => { + return { ...defaultHeaders, ...headers }; + }; + + return { + get: useCallback( + async (url: string, params: object = {}, headers: object = {}) => { + return httpGet(getUrl(url), params, getHeaders(headers)) + .then(handleResponse) + .catch(handleError); + }, + [httpGet], + ), + + post: useCallback( + async (url: string, params: object = {}, headers: object = {}) => { + return httpPost(getUrl(url), params, getHeaders(headers)) + .then(handleResponse) + .catch(handleError); + }, + [httpPost], + ), + + put: useCallback( + async (url: string, params: object = {}, headers: object = {}) => { + return httpPut(getUrl(url), params, getHeaders(headers)) + .then(handleResponse) + .catch(handleError); + }, + [httpPut], + ), + + del: useCallback( + async (url: string, params: object = {}, headers: object = {}) => { + return httpDelete(getUrl(url), params, getHeaders(headers)) + .then(handleResponse) + .catch(handleError); + }, + [httpDelete], + ), + ...reactQuery, + }; } export default useApi; diff --git a/src/components/hooks/useCountryNames.ts b/src/components/hooks/useCountryNames.ts index 2bdaa94e..12f2f0dd 100644 --- a/src/components/hooks/useCountryNames.ts +++ b/src/components/hooks/useCountryNames.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { httpGet } from 'next-basics'; +import { httpGet } from '@/lib/fetch'; import enUS from '../../../public/intl/country/en-US.json'; const countryNames = { diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index 23cb6e70..61838980 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -1,9 +1,9 @@ -import { getMinimumUnit, parseDateRange } from 'lib/date'; -import { setItem } from 'next-basics'; -import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from 'lib/constants'; -import websiteStore, { setWebsiteDateRange, setWebsiteDateCompare } from 'store/websites'; -import appStore, { setDateRange } from 'store/app'; -import { DateRange } from 'lib/types'; +import { getMinimumUnit, parseDateRange } from '@/lib/date'; +import { setItem } from '@/lib/storage'; +import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from '@/lib/constants'; +import websiteStore, { setWebsiteDateRange, setWebsiteDateCompare } from '@/store/websites'; +import appStore, { setDateRange } from '@/store/app'; +import { DateRange } from '@/lib/types'; import { useLocale } from './useLocale'; import { useApi } from './useApi'; diff --git a/src/components/hooks/useFilters.ts b/src/components/hooks/useFilters.ts index 5f89eca4..2b99785a 100644 --- a/src/components/hooks/useFilters.ts +++ b/src/components/hooks/useFilters.ts @@ -1,5 +1,5 @@ import { useMessages } from './useMessages'; -import { OPERATORS } from 'lib/constants'; +import { OPERATORS } from '@/lib/constants'; export function useFilters() { const { formatMessage, labels } = useMessages(); diff --git a/src/components/hooks/useFormat.ts b/src/components/hooks/useFormat.ts index 10030721..927e21e8 100644 --- a/src/components/hooks/useFormat.ts +++ b/src/components/hooks/useFormat.ts @@ -1,5 +1,5 @@ import useMessages from './useMessages'; -import { BROWSERS, OS_NAMES } from 'lib/constants'; +import { BROWSERS, OS_NAMES } from '@/lib/constants'; import useLocale from './useLocale'; import useCountryNames from './useCountryNames'; import useLanguageNames from './useLanguageNames'; diff --git a/src/components/hooks/useLanguageNames.ts b/src/components/hooks/useLanguageNames.ts index 07b36a2c..8c28d560 100644 --- a/src/components/hooks/useLanguageNames.ts +++ b/src/components/hooks/useLanguageNames.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { httpGet } from 'next-basics'; +import { httpGet } from '@/lib/fetch'; import enUS from '../../../public/intl/language/en-US.json'; const languageNames = { diff --git a/src/components/hooks/useLocale.ts b/src/components/hooks/useLocale.ts index 69e7cc41..863b20a5 100644 --- a/src/components/hooks/useLocale.ts +++ b/src/components/hooks/useLocale.ts @@ -1,8 +1,9 @@ import { useEffect } from 'react'; -import { httpGet, setItem } from 'next-basics'; -import { LOCALE_CONFIG } from 'lib/constants'; -import { getDateLocale, getTextDirection } from 'lib/lang'; -import useStore, { setLocale } from 'store/app'; +import { httpGet } from '@/lib/fetch'; +import { setItem } from '@/lib/storage'; +import { LOCALE_CONFIG } from '@/lib/constants'; +import { getDateLocale, getTextDirection } from '@/lib/lang'; +import useStore, { setLocale } from '@/store/app'; import { useForceUpdate } from './useForceUpdate'; import enUS from '../../../public/intl/country/en-US.json'; @@ -19,13 +20,9 @@ export function useLocale() { const dateLocale = getDateLocale(locale); async function loadMessages(locale: string) { - const { ok, data } = await httpGet( - `${process.env.basePath || ''}/intl/messages/${locale}.json`, - ); + const { data } = await httpGet(`${process.env.basePath || ''}/intl/messages/${locale}.json`); - if (ok) { - messages[locale] = data; - } + messages[locale] = data; } async function saveLocale(value: string) { diff --git a/src/components/hooks/useMessages.ts b/src/components/hooks/useMessages.ts index ab37cc19..fc73494f 100644 --- a/src/components/hooks/useMessages.ts +++ b/src/components/hooks/useMessages.ts @@ -1,5 +1,5 @@ -import { useIntl, FormattedMessage } from 'react-intl'; -import { messages, labels } from 'components/messages'; +import { useIntl } from 'react-intl'; +import { messages, labels } from '@/components/messages'; export function useMessages(): any { const intl = useIntl(); @@ -21,7 +21,7 @@ export function useMessages(): any { return descriptor ? intl.formatMessage(descriptor, values, opts) : null; }; - return { formatMessage, FormattedMessage, messages, labels, getMessage }; + return { formatMessage, messages, labels, getMessage }; } export default useMessages; diff --git a/src/components/hooks/useModified.ts b/src/components/hooks/useModified.ts index 858be87e..fd8dc2e6 100644 --- a/src/components/hooks/useModified.ts +++ b/src/components/hooks/useModified.ts @@ -1,4 +1,4 @@ -import useStore from 'store/modified'; +import useStore from '@/store/modified'; export function useModified(key?: string) { const modified = useStore(state => state?.[key]); diff --git a/src/components/hooks/useNavigation.ts b/src/components/hooks/useNavigation.ts index a2c1167a..b727ee90 100644 --- a/src/components/hooks/useNavigation.ts +++ b/src/components/hooks/useNavigation.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { buildUrl, safeDecodeURIComponent } from 'next-basics'; +import { buildUrl } from '@/lib/url'; export function useNavigation(): { pathname: string; @@ -16,7 +16,7 @@ export function useNavigation(): { const obj = {}; for (const [key, value] of params.entries()) { - obj[key] = safeDecodeURIComponent(value); + obj[key] = value; } return obj; diff --git a/src/components/hooks/usePagedQuery.ts b/src/components/hooks/usePagedQuery.ts index 19471432..b6b06e1c 100644 --- a/src/components/hooks/usePagedQuery.ts +++ b/src/components/hooks/usePagedQuery.ts @@ -1,6 +1,6 @@ import { UseQueryOptions } from '@tanstack/react-query'; import { useState } from 'react'; -import { PageResult, PageParams, PagedQueryResult } from 'lib/types'; +import { PageResult, PageParams, PagedQueryResult } from '@/lib/types'; import { useApi } from './useApi'; import { useNavigation } from './useNavigation'; @@ -11,7 +11,7 @@ export function usePagedQuery({ }: Omit & { queryFn: (params?: object) => any }): PagedQueryResult { const { query: queryParams } = useNavigation(); const [params, setParams] = useState({ - query: '', + search: '', page: +queryParams.page || 1, }); diff --git a/src/components/hooks/useTheme.ts b/src/components/hooks/useTheme.ts index aa2b1d38..9bbe063c 100644 --- a/src/components/hooks/useTheme.ts +++ b/src/components/hooks/useTheme.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo } from 'react'; -import useStore, { setTheme } from 'store/app'; -import { getItem, setItem } from 'next-basics'; -import { DEFAULT_THEME, THEME_COLORS, THEME_CONFIG } from 'lib/constants'; +import useStore, { setTheme } from '@/store/app'; +import { getItem, setItem } from '@/lib/storage'; +import { DEFAULT_THEME, THEME_COLORS, THEME_CONFIG } from '@/lib/constants'; import { colord } from 'colord'; const selector = (state: { theme: string }) => state.theme; diff --git a/src/components/hooks/useTimezone.ts b/src/components/hooks/useTimezone.ts index c74f513f..99c1f115 100644 --- a/src/components/hooks/useTimezone.ts +++ b/src/components/hooks/useTimezone.ts @@ -1,12 +1,14 @@ -import { setItem } from 'next-basics'; -import { TIMEZONE_CONFIG } from 'lib/constants'; +import { setItem } from '@/lib/storage'; +import { TIMEZONE_CONFIG } from '@/lib/constants'; import { formatInTimeZone, zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz'; -import useStore, { setTimezone } from 'store/app'; +import useStore, { setTimezone } from '@/store/app'; +import useLocale from './useLocale'; const selector = (state: { timezone: string }) => state.timezone; export function useTimezone() { const timezone = useStore(selector); + const { dateLocale } = useLocale(); const saveTimezone = (value: string) => { setItem(TIMEZONE_CONFIG, value); @@ -20,6 +22,7 @@ export function useTimezone() { : date.split(' ').join('T') + 'Z', timezone, pattern, + { locale: dateLocale }, ); }; diff --git a/src/components/icons.ts b/src/components/icons.ts index 1cf26543..e952e500 100644 --- a/src/components/icons.ts +++ b/src/components/icons.ts @@ -1,30 +1,30 @@ import { Icons } from 'react-basics'; -import AddUser from 'assets/add-user.svg'; -import Bars from 'assets/bars.svg'; -import BarChart from 'assets/bar-chart.svg'; -import Bolt from 'assets/bolt.svg'; -import Calendar from 'assets/calendar.svg'; -import Change from 'assets/change.svg'; -import Clock from 'assets/clock.svg'; -import Compare from 'assets/compare.svg'; -import Dashboard from 'assets/dashboard.svg'; -import Eye from 'assets/eye.svg'; -import Gear from 'assets/gear.svg'; -import Globe from 'assets/globe.svg'; -import Location from 'assets/location.svg'; -import Lock from 'assets/lock.svg'; -import Logo from 'assets/logo.svg'; -import Magnet from 'assets/magnet.svg'; -import Moon from 'assets/moon.svg'; -import Nodes from 'assets/nodes.svg'; -import Overview from 'assets/overview.svg'; -import Profile from 'assets/profile.svg'; -import PushPin from 'assets/pushpin.svg'; -import Reports from 'assets/reports.svg'; -import Sun from 'assets/sun.svg'; -import User from 'assets/user.svg'; -import Users from 'assets/users.svg'; -import Visitor from 'assets/visitor.svg'; +import AddUser from '@/assets/add-user.svg'; +import Bars from '@/assets/bars.svg'; +import BarChart from '@/assets/bar-chart.svg'; +import Bolt from '@/assets/bolt.svg'; +import Calendar from '@/assets/calendar.svg'; +import Change from '@/assets/change.svg'; +import Clock from '@/assets/clock.svg'; +import Compare from '@/assets/compare.svg'; +import Dashboard from '@/assets/dashboard.svg'; +import Eye from '@/assets/eye.svg'; +import Gear from '@/assets/gear.svg'; +import Globe from '@/assets/globe.svg'; +import Location from '@/assets/location.svg'; +import Lock from '@/assets/lock.svg'; +import Logo from '@/assets/logo.svg'; +import Magnet from '@/assets/magnet.svg'; +import Moon from '@/assets/moon.svg'; +import Nodes from '@/assets/nodes.svg'; +import Overview from '@/assets/overview.svg'; +import Profile from '@/assets/profile.svg'; +import PushPin from '@/assets/pushpin.svg'; +import Reports from '@/assets/reports.svg'; +import Sun from '@/assets/sun.svg'; +import User from '@/assets/user.svg'; +import Users from '@/assets/users.svg'; +import Visitor from '@/assets/visitor.svg'; const icons = { ...Icons, diff --git a/src/components/input/DateFilter.tsx b/src/components/input/DateFilter.tsx index e486551d..443827a0 100644 --- a/src/components/input/DateFilter.tsx +++ b/src/components/input/DateFilter.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics'; import { endOfYear, isSameDay } from 'date-fns'; -import DatePickerForm from 'components/metrics/DatePickerForm'; -import { useLocale, useMessages } from 'components/hooks'; -import Icons from 'components/icons'; -import { formatDate, parseDateValue } from 'lib/date'; +import DatePickerForm from '@/components/metrics/DatePickerForm'; +import { useLocale, useMessages } from '@/components/hooks'; +import Icons from '@/components/icons'; +import { formatDate, parseDateValue } from '@/lib/date'; import styles from './DateFilter.module.css'; import classNames from 'classnames'; diff --git a/src/components/input/LanguageButton.tsx b/src/components/input/LanguageButton.tsx index 5da3bf78..54ce55eb 100644 --- a/src/components/input/LanguageButton.tsx +++ b/src/components/input/LanguageButton.tsx @@ -1,8 +1,8 @@ import { Icon, Button, PopupTrigger, Popup } from 'react-basics'; import classNames from 'classnames'; -import { languages } from 'lib/lang'; -import { useLocale } from 'components/hooks'; -import Icons from 'components/icons'; +import { languages } from '@/lib/lang'; +import { useLocale } from '@/components/hooks'; +import Icons from '@/components/icons'; import styles from './LanguageButton.module.css'; export function LanguageButton() { diff --git a/src/components/input/LogoutButton.tsx b/src/components/input/LogoutButton.tsx index ddc71142..a1a34a00 100644 --- a/src/components/input/LogoutButton.tsx +++ b/src/components/input/LogoutButton.tsx @@ -1,6 +1,6 @@ import { Button, Icon, Icons, TooltipPopup } from 'react-basics'; import Link from 'next/link'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export function LogoutButton({ tooltipPosition = 'top', diff --git a/src/components/input/MonthSelect.tsx b/src/components/input/MonthSelect.tsx index acb17dfe..144f5bd8 100644 --- a/src/components/input/MonthSelect.tsx +++ b/src/components/input/MonthSelect.tsx @@ -9,9 +9,9 @@ import { Popup, } from 'react-basics'; import { startOfMonth, endOfMonth } from 'date-fns'; -import Icons from 'components/icons'; -import { useLocale } from 'components/hooks'; -import { formatDate } from 'lib/date'; +import Icons from '@/components/icons'; +import { useLocale } from '@/components/hooks'; +import { formatDate } from '@/lib/date'; import styles from './MonthSelect.module.css'; export function MonthSelect({ date = new Date(), onChange }) { diff --git a/src/components/input/ProfileButton.tsx b/src/components/input/ProfileButton.tsx index b1875165..86a9d333 100644 --- a/src/components/input/ProfileButton.tsx +++ b/src/components/input/ProfileButton.tsx @@ -1,9 +1,9 @@ import { Key } from 'react'; import { Icon, Button, PopupTrigger, Popup, Menu, Item, Text } from 'react-basics'; import { useRouter } from 'next/navigation'; -import Icons from 'components/icons'; -import { useMessages, useLogin, useLocale } from 'components/hooks'; -import { CURRENT_VERSION } from 'lib/constants'; +import Icons from '@/components/icons'; +import { useMessages, useLogin, useLocale } from '@/components/hooks'; +import { CURRENT_VERSION } from '@/lib/constants'; import styles from './ProfileButton.module.css'; export function ProfileButton() { diff --git a/src/components/input/RefreshButton.tsx b/src/components/input/RefreshButton.tsx index cd68c40a..35bfbf3c 100644 --- a/src/components/input/RefreshButton.tsx +++ b/src/components/input/RefreshButton.tsx @@ -1,8 +1,8 @@ import { LoadingButton, Icon, TooltipPopup } from 'react-basics'; -import { setWebsiteDateRange } from 'store/websites'; -import { useDateRange } from 'components/hooks'; -import Icons from 'components/icons'; -import { useMessages } from 'components/hooks'; +import { setWebsiteDateRange } from '@/store/websites'; +import { useDateRange } from '@/components/hooks'; +import Icons from '@/components/icons'; +import { useMessages } from '@/components/hooks'; export function RefreshButton({ websiteId, diff --git a/src/components/input/SettingsButton.tsx b/src/components/input/SettingsButton.tsx index 535d03c3..d3dc471f 100644 --- a/src/components/input/SettingsButton.tsx +++ b/src/components/input/SettingsButton.tsx @@ -1,8 +1,8 @@ import { Button, Icon, PopupTrigger, Popup, Form, FormRow } from 'react-basics'; -import TimezoneSetting from 'app/(main)/profile/TimezoneSetting'; -import DateRangeSetting from 'app/(main)/profile/DateRangeSetting'; -import Icons from 'components/icons'; -import { useMessages } from 'components/hooks'; +import TimezoneSetting from '@/app/(main)/profile/TimezoneSetting'; +import DateRangeSetting from '@/app/(main)/profile/DateRangeSetting'; +import Icons from '@/components/icons'; +import { useMessages } from '@/components/hooks'; import styles from './SettingsButton.module.css'; export function SettingsButton() { diff --git a/src/components/input/TeamsButton.tsx b/src/components/input/TeamsButton.tsx index 1f6270b4..f967a64c 100644 --- a/src/components/input/TeamsButton.tsx +++ b/src/components/input/TeamsButton.tsx @@ -1,8 +1,8 @@ import { Key } from 'react'; import { Text, Icon, Button, Popup, Menu, Item, PopupTrigger, Flexbox } from 'react-basics'; import classNames from 'classnames'; -import Icons from 'components/icons'; -import { useLogin, useMessages, useTeams, useTeamUrl } from 'components/hooks'; +import Icons from '@/components/icons'; +import { useLogin, useMessages, useTeams, useTeamUrl } from '@/components/hooks'; import styles from './TeamsButton.module.css'; export function TeamsButton({ @@ -16,7 +16,7 @@ export function TeamsButton({ }) { const { user } = useLogin(); const { formatMessage, labels } = useMessages(); - const { result } = useTeams(user?.id); + const { result } = useTeams(user.id); const { teamId } = useTeamUrl(); const team = result?.data?.find(({ id }) => id === teamId); diff --git a/src/components/input/ThemeButton.tsx b/src/components/input/ThemeButton.tsx index ece571ab..fd7d79a0 100644 --- a/src/components/input/ThemeButton.tsx +++ b/src/components/input/ThemeButton.tsx @@ -1,7 +1,7 @@ import { useTransition, animated } from '@react-spring/web'; import { Button, Icon } from 'react-basics'; -import { useTheme } from 'components/hooks'; -import Icons from 'components/icons'; +import { useTheme } from '@/components/hooks'; +import Icons from '@/components/icons'; import styles from './ThemeButton.module.css'; export function ThemeButton() { @@ -28,7 +28,7 @@ export function ThemeButton() { diff --git a/src/components/input/WebsiteDateFilter.tsx b/src/components/input/WebsiteDateFilter.tsx index 486f5de1..97beaf12 100644 --- a/src/components/input/WebsiteDateFilter.tsx +++ b/src/components/input/WebsiteDateFilter.tsx @@ -1,10 +1,10 @@ -import { useDateRange, useLocale } from 'components/hooks'; +import { useDateRange, useLocale } from '@/components/hooks'; import { isAfter } from 'date-fns'; -import { getOffsetDateRange } from 'lib/date'; +import { getOffsetDateRange } from '@/lib/date'; import { Button, Icon, Icons } from 'react-basics'; import DateFilter from './DateFilter'; import styles from './WebsiteDateFilter.module.css'; -import { DateRange } from 'lib/types'; +import { DateRange } from '@/lib/types'; export function WebsiteDateFilter({ websiteId, diff --git a/src/components/input/WebsiteSelect.tsx b/src/components/input/WebsiteSelect.tsx index 0540ed38..8a7e4ac0 100644 --- a/src/components/input/WebsiteSelect.tsx +++ b/src/components/input/WebsiteSelect.tsx @@ -1,7 +1,7 @@ import { useState, Key } from 'react'; import { Dropdown, Item } from 'react-basics'; -import { useWebsite, useWebsites, useMessages } from 'components/hooks'; -import Empty from 'components/common/Empty'; +import { useWebsite, useWebsites, useMessages } from '@/components/hooks'; +import Empty from '@/components/common/Empty'; import styles from './WebsiteSelect.module.css'; export function WebsiteSelect({ @@ -14,12 +14,12 @@ export function WebsiteSelect({ onSelect?: (key: any) => void; }) { const { formatMessage, labels, messages } = useMessages(); - const [query, setQuery] = useState(''); + const [search, setSearch] = useState(''); const [selectedId, setSelectedId] = useState(websiteId); const { data: website } = useWebsite(selectedId as string); - const queryResult = useWebsites({ teamId }, { query, pageSize: 5 }); + const queryResult = useWebsites({ teamId }, { search, pageSize: 5 }); const renderValue = () => { return website?.name; @@ -35,7 +35,7 @@ export function WebsiteSelect({ }; const handleSearch = (value: string) => { - setQuery(value); + setSearch(value); }; return ( diff --git a/src/components/layout/MenuLayout.tsx b/src/components/layout/MenuLayout.tsx index 2edd1091..1465c062 100644 --- a/src/components/layout/MenuLayout.tsx +++ b/src/components/layout/MenuLayout.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; import { usePathname } from 'next/navigation'; -import SideNav from 'components/layout/SideNav'; +import SideNav from '@/components/layout/SideNav'; import styles from './MenuLayout.module.css'; export function MenuLayout({ items = [], children }: { items: any[]; children: ReactNode }) { diff --git a/src/components/layout/NavGroup.module.css b/src/components/layout/NavGroup.module.css index d827da86..4979210a 100644 --- a/src/components/layout/NavGroup.module.css +++ b/src/components/layout/NavGroup.module.css @@ -51,11 +51,6 @@ a.item { color: var(--base900); } -.item.disabled { - color: var(--base500) !important; - pointer-events: none; -} - .minimized .text, .minimized .header { display: none; diff --git a/src/components/layout/NavGroup.tsx b/src/components/layout/NavGroup.tsx index e95b61fa..723f9a7e 100644 --- a/src/components/layout/NavGroup.tsx +++ b/src/components/layout/NavGroup.tsx @@ -3,7 +3,7 @@ import { Icon, Text, TooltipPopup } from 'react-basics'; import classNames from 'classnames'; import { usePathname } from 'next/navigation'; import Link from 'next/link'; -import Icons from 'components/icons'; +import Icons from '@/components/icons'; import styles from './NavGroup.module.css'; export interface NavGroupProps { diff --git a/src/components/layout/Page.tsx b/src/components/layout/Page.tsx index 83312d12..c06054b4 100644 --- a/src/components/layout/Page.tsx +++ b/src/components/layout/Page.tsx @@ -2,7 +2,7 @@ import { ReactNode } from 'react'; import classNames from 'classnames'; import { Banner, Loading } from 'react-basics'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './Page.module.css'; export function Page({ diff --git a/src/components/messages.ts b/src/components/messages.ts index 688dd11d..5279e1b4 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -280,6 +280,23 @@ export const labels = defineMessages({ lastSeen: { id: 'label.last-seen', defaultMessage: 'Last seen' }, firstSeen: { id: 'label.first-seen', defaultMessage: 'First seen' }, properties: { id: 'label.properties', defaultMessage: 'Properties' }, + channels: { id: 'label.channels', defaultMessage: 'Channels' }, + direct: { id: 'label.direct', defaultMessage: 'Direct' }, + referral: { id: 'label.referral', defaultMessage: 'Referral' }, + affiliate: { id: 'label.affiliate', defaultMessage: 'Affiliate' }, + email: { id: 'label.email', defaultMessage: 'Email' }, + sms: { id: 'label.sms', defaultMessage: 'SMS' }, + organicSearch: { id: 'label.organic-search', defaultMessage: 'Organic search' }, + organicSocial: { id: 'label.organic-social', defaultMessage: 'Organic social' }, + organicShopping: { id: 'label.organic-shopping', defaultMessage: 'Organic shopping' }, + organicVideo: { id: 'label.organic-video', defaultMessage: 'Organic video' }, + paidAds: { id: 'label.paid-ads', defaultMessage: 'Paid ads' }, + paidSearch: { id: 'label.paid-search', defaultMessage: 'Paid search' }, + paidSocial: { id: 'label.paid-social', defaultMessage: 'Paid social' }, + paidShopping: { id: 'label.paid-shopping', defaultMessage: 'Paid shopping' }, + paidVideo: { id: 'label.paid-video', defaultMessage: 'Paid video' }, + grouped: { id: 'label.grouped', defaultMessage: 'Grouped' }, + other: { id: 'label.other', defaultMessage: 'Other' }, }); export const messages = defineMessages({ diff --git a/src/components/metrics/ActiveUsers.module.css b/src/components/metrics/ActiveUsers.module.css index 5d0a4c7d..4a984725 100644 --- a/src/components/metrics/ActiveUsers.module.css +++ b/src/components/metrics/ActiveUsers.module.css @@ -10,8 +10,3 @@ font-size: var(--font-size-md); font-weight: 400; } - -.value { - font-weight: 600; - margin-inline-end: 4px; -} diff --git a/src/components/metrics/ActiveUsers.tsx b/src/components/metrics/ActiveUsers.tsx index 05d0fc1d..50c676ab 100644 --- a/src/components/metrics/ActiveUsers.tsx +++ b/src/components/metrics/ActiveUsers.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { StatusLight } from 'react-basics'; -import { useApi } from 'components/hooks'; -import { useMessages } from 'components/hooks'; +import { useApi } from '@/components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './ActiveUsers.module.css'; export function ActiveUsers({ @@ -24,7 +24,7 @@ export function ActiveUsers({ const count = useMemo(() => { if (websiteId) { - return data?.x || 0; + return data?.visitors || 0; } return value !== undefined ? value : 0; diff --git a/src/components/metrics/BrowsersTable.tsx b/src/components/metrics/BrowsersTable.tsx index d0cec124..500686b1 100644 --- a/src/components/metrics/BrowsersTable.tsx +++ b/src/components/metrics/BrowsersTable.tsx @@ -1,8 +1,8 @@ -import FilterLink from 'components/common/FilterLink'; -import MetricsTable, { MetricsTableProps } from 'components/metrics/MetricsTable'; -import { useMessages } from 'components/hooks'; -import { useFormat } from 'components/hooks'; -import TypeIcon from 'components/common/TypeIcon'; +import FilterLink from '@/components/common/FilterLink'; +import MetricsTable, { MetricsTableProps } from '@/components/metrics/MetricsTable'; +import { useMessages } from '@/components/hooks'; +import { useFormat } from '@/components/hooks'; +import TypeIcon from '@/components/common/TypeIcon'; export function BrowsersTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/ChannelsTable.tsx b/src/components/metrics/ChannelsTable.tsx new file mode 100644 index 00000000..d2dc207f --- /dev/null +++ b/src/components/metrics/ChannelsTable.tsx @@ -0,0 +1,22 @@ +import MetricsTable, { MetricsTableProps } from '@/components/metrics/MetricsTable'; +import { useMessages } from '@/components/hooks'; + +export function ChannelsTable(props: MetricsTableProps) { + const { formatMessage, labels } = useMessages(); + + const renderLabel = ({ x }) => { + return formatMessage(labels[x]); + }; + + return ( + + ); +} + +export default ChannelsTable; diff --git a/src/components/metrics/CitiesTable.tsx b/src/components/metrics/CitiesTable.tsx index fd628e7f..1e5fc735 100644 --- a/src/components/metrics/CitiesTable.tsx +++ b/src/components/metrics/CitiesTable.tsx @@ -1,8 +1,8 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import { emptyFilter } from 'lib/filters'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages } from 'components/hooks'; -import { useFormat } from 'components/hooks'; +import { emptyFilter } from '@/lib/filters'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages } from '@/components/hooks'; +import { useFormat } from '@/components/hooks'; export function CitiesTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/CountriesTable.tsx b/src/components/metrics/CountriesTable.tsx index f4560ae3..cdd05115 100644 --- a/src/components/metrics/CountriesTable.tsx +++ b/src/components/metrics/CountriesTable.tsx @@ -1,8 +1,8 @@ -import FilterLink from 'components/common/FilterLink'; -import { useCountryNames } from 'components/hooks'; -import { useLocale, useMessages, useFormat } from 'components/hooks'; +import FilterLink from '@/components/common/FilterLink'; +import { useCountryNames } from '@/components/hooks'; +import { useLocale, useMessages, useFormat } from '@/components/hooks'; import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import TypeIcon from 'components/common/TypeIcon'; +import TypeIcon from '@/components/common/TypeIcon'; export function CountriesTable({ ...props }: MetricsTableProps) { const { locale } = useLocale(); diff --git a/src/components/metrics/DatePickerForm.tsx b/src/components/metrics/DatePickerForm.tsx index 892cd127..d1a5c7db 100644 --- a/src/components/metrics/DatePickerForm.tsx +++ b/src/components/metrics/DatePickerForm.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; import { Button, ButtonGroup, Calendar } from 'react-basics'; import { isAfter, isBefore, isSameDay, startOfDay, endOfDay } from 'date-fns'; -import { useLocale } from 'components/hooks'; -import { FILTER_DAY, FILTER_RANGE } from 'lib/constants'; -import { useMessages } from 'components/hooks'; +import { useLocale } from '@/components/hooks'; +import { FILTER_DAY, FILTER_RANGE } from '@/lib/constants'; +import { useMessages } from '@/components/hooks'; import styles from './DatePickerForm.module.css'; export function DatePickerForm({ diff --git a/src/components/metrics/DevicesTable.tsx b/src/components/metrics/DevicesTable.tsx index c25afe4f..ed327c33 100644 --- a/src/components/metrics/DevicesTable.tsx +++ b/src/components/metrics/DevicesTable.tsx @@ -1,8 +1,8 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages } from 'components/hooks'; -import { useFormat } from 'components/hooks'; -import TypeIcon from 'components/common/TypeIcon'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages } from '@/components/hooks'; +import { useFormat } from '@/components/hooks'; +import TypeIcon from '@/components/common/TypeIcon'; export function DevicesTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/EventsChart.tsx b/src/components/metrics/EventsChart.tsx index 2ba2caee..9655c4a4 100644 --- a/src/components/metrics/EventsChart.tsx +++ b/src/components/metrics/EventsChart.tsx @@ -1,8 +1,8 @@ import { colord } from 'colord'; -import BarChart from 'components/charts/BarChart'; -import { useDateRange, useLocale, useWebsiteEventsSeries } from 'components/hooks'; -import { renderDateLabels } from 'lib/charts'; -import { CHART_COLORS } from 'lib/constants'; +import BarChart from '@/components/charts/BarChart'; +import { useDateRange, useLocale, useWebsiteEventsSeries } from '@/components/hooks'; +import { renderDateLabels } from '@/lib/charts'; +import { CHART_COLORS } from '@/lib/constants'; import { useMemo } from 'react'; export interface EventsChartProps { diff --git a/src/components/metrics/EventsTable.tsx b/src/components/metrics/EventsTable.tsx index c90ae988..bc753b3b 100644 --- a/src/components/metrics/EventsTable.tsx +++ b/src/components/metrics/EventsTable.tsx @@ -1,5 +1,5 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export function EventsTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/FilterTags.tsx b/src/components/metrics/FilterTags.tsx index 60cf90c1..fcba3c9e 100644 --- a/src/components/metrics/FilterTags.tsx +++ b/src/components/metrics/FilterTags.tsx @@ -7,13 +7,13 @@ import { useMessages, useFormat, useFilters, -} from 'components/hooks'; -import PopupForm from 'app/(main)/reports/[reportId]/PopupForm'; -import FieldFilterEditForm from 'app/(main)/reports/[reportId]/FieldFilterEditForm'; -import { OPERATOR_PREFIXES } from 'lib/constants'; -import { isSearchOperator, parseParameterValue } from 'lib/params'; +} from '@/components/hooks'; +import PopupForm from '@/app/(main)/reports/[reportId]/PopupForm'; +import FieldFilterEditForm from '@/app/(main)/reports/[reportId]/FieldFilterEditForm'; +import { OPERATOR_PREFIXES } from '@/lib/constants'; +import { isSearchOperator, parseParameterValue } from '@/lib/params'; import styles from './FilterTags.module.css'; -import WebsiteFilterButton from 'app/(main)/websites/[websiteId]/WebsiteFilterButton'; +import WebsiteFilterButton from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; export function FilterTags({ websiteId, diff --git a/src/components/metrics/HostsTable.tsx b/src/components/metrics/HostsTable.tsx index 45147eac..e034b970 100644 --- a/src/components/metrics/HostsTable.tsx +++ b/src/components/metrics/HostsTable.tsx @@ -1,6 +1,6 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages } from 'components/hooks'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages } from '@/components/hooks'; import { Flexbox } from 'react-basics'; export function HostsTable(props: MetricsTableProps) { diff --git a/src/components/metrics/LanguagesTable.tsx b/src/components/metrics/LanguagesTable.tsx index 24b62046..3ced249e 100644 --- a/src/components/metrics/LanguagesTable.tsx +++ b/src/components/metrics/LanguagesTable.tsx @@ -1,8 +1,8 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import { percentFilter } from 'lib/filters'; -import { useLocale } from 'components/hooks'; -import { useMessages } from 'components/hooks'; -import { useFormat } from 'components/hooks'; +import { percentFilter } from '@/lib/filters'; +import { useLocale } from '@/components/hooks'; +import { useMessages } from '@/components/hooks'; +import { useFormat } from '@/components/hooks'; export function LanguagesTable({ onDataLoad, diff --git a/src/components/metrics/Legend.tsx b/src/components/metrics/Legend.tsx index 4ebcf4b4..77442957 100644 --- a/src/components/metrics/Legend.tsx +++ b/src/components/metrics/Legend.tsx @@ -1,5 +1,4 @@ import { StatusLight } from 'react-basics'; -import { safeDecodeURIComponent } from 'next-basics'; import { colord } from 'colord'; import classNames from 'classnames'; import { LegendItem } from 'chart.js/auto'; @@ -28,9 +27,7 @@ export function Legend({ className={classNames(styles.label, { [styles.hidden]: hidden })} onClick={() => onClick(item)} > - - {safeDecodeURIComponent(text)} - + {text}
); })} diff --git a/src/components/metrics/ListTable.tsx b/src/components/metrics/ListTable.tsx index 59ded491..6fbf390a 100644 --- a/src/components/metrics/ListTable.tsx +++ b/src/components/metrics/ListTable.tsx @@ -1,9 +1,9 @@ import { FixedSizeList } from 'react-window'; import { useSpring, animated, config } from '@react-spring/web'; import classNames from 'classnames'; -import Empty from 'components/common/Empty'; -import { formatLongNumber } from 'lib/format'; -import { useMessages } from 'components/hooks'; +import Empty from '@/components/common/Empty'; +import { formatLongNumber } from '@/lib/format'; +import { useMessages } from '@/components/hooks'; import styles from './ListTable.module.css'; import { ReactNode } from 'react'; diff --git a/src/components/metrics/MetricCard.tsx b/src/components/metrics/MetricCard.tsx index 64f2a1b6..41766167 100644 --- a/src/components/metrics/MetricCard.tsx +++ b/src/components/metrics/MetricCard.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import { useSpring, animated } from '@react-spring/web'; -import { formatNumber } from 'lib/format'; -import ChangeLabel from 'components/metrics/ChangeLabel'; +import { formatNumber } from '@/lib/format'; +import ChangeLabel from '@/components/metrics/ChangeLabel'; import styles from './MetricCard.module.css'; export interface MetricCardProps { diff --git a/src/components/metrics/MetricsBar.tsx b/src/components/metrics/MetricsBar.tsx index 60a21706..6e9f22de 100644 --- a/src/components/metrics/MetricsBar.tsx +++ b/src/components/metrics/MetricsBar.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react'; import { Loading, cloneChildren } from 'react-basics'; -import ErrorMessage from 'components/common/ErrorMessage'; -import { formatLongNumber } from 'lib/format'; +import ErrorMessage from '@/components/common/ErrorMessage'; +import { formatLongNumber } from '@/lib/format'; import styles from './MetricsBar.module.css'; export interface MetricsBarProps { diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index 4db599b9..616262cb 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -1,18 +1,18 @@ import { ReactNode, useMemo, useState } from 'react'; import { Loading, Icon, Text, SearchField } from 'react-basics'; import classNames from 'classnames'; -import ErrorMessage from 'components/common/ErrorMessage'; -import LinkButton from 'components/common/LinkButton'; -import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; -import { percentFilter } from 'lib/filters'; +import ErrorMessage from '@/components/common/ErrorMessage'; +import LinkButton from '@/components/common/LinkButton'; +import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants'; +import { percentFilter } from '@/lib/filters'; import { useNavigation, useWebsiteMetrics, useMessages, useLocale, useFormat, -} from 'components/hooks'; -import Icons from 'components/icons'; +} from '@/components/hooks'; +import Icons from '@/components/icons'; import ListTable, { ListTableProps } from './ListTable'; import styles from './MetricsTable.module.css'; @@ -72,7 +72,7 @@ export function MetricsTable({ return filter(arr); }, items); } else { - items = dataFilter(data); + items = dataFilter(items); } } diff --git a/src/components/metrics/OSTable.tsx b/src/components/metrics/OSTable.tsx index 6989504c..37b79549 100644 --- a/src/components/metrics/OSTable.tsx +++ b/src/components/metrics/OSTable.tsx @@ -1,7 +1,7 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages, useFormat } from 'components/hooks'; -import TypeIcon from 'components/common/TypeIcon'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages, useFormat } from '@/components/hooks'; +import TypeIcon from '@/components/common/TypeIcon'; export function OSTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/PagesTable.tsx b/src/components/metrics/PagesTable.tsx index b2d8ca9c..8163b3d9 100644 --- a/src/components/metrics/PagesTable.tsx +++ b/src/components/metrics/PagesTable.tsx @@ -1,8 +1,8 @@ -import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; -import FilterButtons from 'components/common/FilterButtons'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages, useNavigation } from 'components/hooks'; -import { emptyFilter } from 'lib/filters'; +import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; +import FilterButtons from '@/components/common/FilterButtons'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages, useNavigation } from '@/components/hooks'; +import { emptyFilter } from '@/lib/filters'; import { useContext } from 'react'; import MetricsTable, { MetricsTableProps } from './MetricsTable'; diff --git a/src/components/metrics/PageviewsChart.tsx b/src/components/metrics/PageviewsChart.tsx index 6274defc..6fa3285f 100644 --- a/src/components/metrics/PageviewsChart.tsx +++ b/src/components/metrics/PageviewsChart.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; -import BarChart, { BarChartProps } from 'components/charts/BarChart'; -import { useLocale, useTheme, useMessages } from 'components/hooks'; -import { renderDateLabels } from 'lib/charts'; +import BarChart, { BarChartProps } from '@/components/charts/BarChart'; +import { useLocale, useTheme, useMessages } from '@/components/hooks'; +import { renderDateLabels } from '@/lib/charts'; export interface PagepageviewsChartProps extends BarChartProps { data: { diff --git a/src/components/metrics/QueryParametersTable.tsx b/src/components/metrics/QueryParametersTable.tsx index f0d08ecf..26f01faf 100644 --- a/src/components/metrics/QueryParametersTable.tsx +++ b/src/components/metrics/QueryParametersTable.tsx @@ -1,10 +1,9 @@ import { useState } from 'react'; -import { safeDecodeURI } from 'next-basics'; -import FilterButtons from 'components/common/FilterButtons'; -import { emptyFilter, paramFilter } from 'lib/filters'; -import { FILTER_RAW, FILTER_COMBINED } from 'lib/constants'; +import FilterButtons from '@/components/common/FilterButtons'; +import { emptyFilter, paramFilter } from '@/lib/filters'; +import { FILTER_RAW, FILTER_COMBINED } from '@/lib/constants'; import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; import styles from './QueryParametersTable.module.css'; const filters = { @@ -39,8 +38,8 @@ export function QueryParametersTable({ x ) : (
-
{safeDecodeURI(p)}
-
{safeDecodeURI(v)}
+
{p}
+
{v}
) } diff --git a/src/components/metrics/RealtimeChart.tsx b/src/components/metrics/RealtimeChart.tsx index b2819f9c..f5697caa 100644 --- a/src/components/metrics/RealtimeChart.tsx +++ b/src/components/metrics/RealtimeChart.tsx @@ -1,8 +1,8 @@ import { useMemo, useRef } from 'react'; import { startOfMinute, subMinutes, isBefore } from 'date-fns'; import PageviewsChart from './PageviewsChart'; -import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants'; -import { RealtimeData } from 'lib/types'; +import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from '@/lib/constants'; +import { RealtimeData } from '@/lib/types'; export interface RealtimeChartProps { data: RealtimeData; diff --git a/src/components/metrics/ReferrersTable.tsx b/src/components/metrics/ReferrersTable.tsx index d83c4d12..db40a617 100644 --- a/src/components/metrics/ReferrersTable.tsx +++ b/src/components/metrics/ReferrersTable.tsx @@ -1,12 +1,53 @@ -import FilterLink from 'components/common/FilterLink'; -import Favicon from 'components/common/Favicon'; -import { useMessages } from 'components/hooks'; +import FilterLink from '@/components/common/FilterLink'; +import Favicon from '@/components/common/Favicon'; +import { useMessages, useNavigation } from '@/components/hooks'; import MetricsTable, { MetricsTableProps } from './MetricsTable'; +import FilterButtons from '@/components/common/FilterButtons'; +import thenby from 'thenby'; +import { GROUPED_DOMAINS } from '@/lib/constants'; +import { Flexbox } from 'react-basics'; -export function ReferrersTable(props: MetricsTableProps) { +export interface ReferrersTableProps extends MetricsTableProps { + allowFilter?: boolean; +} + +export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) { + const { + router, + renderUrl, + query: { view = 'referrer' }, + } = useNavigation(); const { formatMessage, labels } = useMessages(); + const handleSelect = (key: any) => { + router.push(renderUrl({ view: key }), { scroll: false }); + }; + + const buttons = [ + { + label: formatMessage(labels.domain), + key: 'referrer', + }, + { + label: formatMessage(labels.grouped), + key: 'grouped', + }, + ]; + const renderLink = ({ x: referrer }) => { + if (view === 'grouped') { + if (referrer === '_other') { + return `(${formatMessage(labels.other)})`; + } else { + return ( + + + {GROUPED_DOMAINS.find(({ domain }) => domain === referrer)?.name} + + ); + } + } + return ( { + const groups = { _other: 0 }; + + for (const { x, y } of data) { + for (const { domain, match } of GROUPED_DOMAINS) { + if (Array.isArray(match) ? match.some(str => x.includes(str)) : x.includes(match)) { + if (!groups[domain]) { + groups[domain] = 0; + } + groups[domain] += y; + } else { + groups._other += y; + } + } + } + + return Object.keys(groups) + .map((key: any) => ({ x: key, y: groups[key] })) + .sort(thenby.firstBy('y', -1)); + }; + return ( <> + > + {allowFilter && ( + + )} + ); } diff --git a/src/components/metrics/RegionsTable.tsx b/src/components/metrics/RegionsTable.tsx index 0c3a931f..0b7e3bdf 100644 --- a/src/components/metrics/RegionsTable.tsx +++ b/src/components/metrics/RegionsTable.tsx @@ -1,8 +1,8 @@ -import FilterLink from 'components/common/FilterLink'; -import { emptyFilter } from 'lib/filters'; -import { useMessages, useLocale, useRegionNames } from 'components/hooks'; +import FilterLink from '@/components/common/FilterLink'; +import { emptyFilter } from '@/lib/filters'; +import { useMessages, useLocale, useRegionNames } from '@/components/hooks'; import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import TypeIcon from 'components/common/TypeIcon'; +import TypeIcon from '@/components/common/TypeIcon'; export function RegionsTable(props: MetricsTableProps) { const { locale } = useLocale(); diff --git a/src/components/metrics/ScreenTable.tsx b/src/components/metrics/ScreenTable.tsx index 51015fcb..c2a19caa 100644 --- a/src/components/metrics/ScreenTable.tsx +++ b/src/components/metrics/ScreenTable.tsx @@ -1,5 +1,5 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import { useMessages } from 'components/hooks'; +import { useMessages } from '@/components/hooks'; export function ScreenTable(props: MetricsTableProps) { const { formatMessage, labels } = useMessages(); diff --git a/src/components/metrics/TagsTable.tsx b/src/components/metrics/TagsTable.tsx index a1130bb4..e915f873 100644 --- a/src/components/metrics/TagsTable.tsx +++ b/src/components/metrics/TagsTable.tsx @@ -1,6 +1,6 @@ import MetricsTable, { MetricsTableProps } from './MetricsTable'; -import FilterLink from 'components/common/FilterLink'; -import { useMessages } from 'components/hooks'; +import FilterLink from '@/components/common/FilterLink'; +import { useMessages } from '@/components/hooks'; import { Flexbox } from 'react-basics'; export function TagsTable(props: MetricsTableProps) { diff --git a/src/components/metrics/WorldMap.tsx b/src/components/metrics/WorldMap.tsx index 5dfc5f74..a377bfc9 100644 --- a/src/components/metrics/WorldMap.tsx +++ b/src/components/metrics/WorldMap.tsx @@ -2,14 +2,14 @@ import { useState, useMemo, HTMLAttributes } from 'react'; import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps'; import classNames from 'classnames'; import { colord } from 'colord'; -import HoverTooltip from 'components/common/HoverTooltip'; -import { ISO_COUNTRIES, MAP_FILE } from 'lib/constants'; -import { useDateRange, useTheme, useWebsiteMetrics } from 'components/hooks'; -import { useCountryNames } from 'components/hooks'; -import { useLocale } from 'components/hooks'; -import { useMessages } from 'components/hooks'; -import { formatLongNumber } from 'lib/format'; -import { percentFilter } from 'lib/filters'; +import HoverTooltip from '@/components/common/HoverTooltip'; +import { ISO_COUNTRIES, MAP_FILE } from '@/lib/constants'; +import { useDateRange, useTheme, useWebsiteMetrics } from '@/components/hooks'; +import { useCountryNames } from '@/components/hooks'; +import { useLocale } from '@/components/hooks'; +import { useMessages } from '@/components/hooks'; +import { formatLongNumber } from '@/lib/format'; +import { percentFilter } from '@/lib/filters'; import styles from './WorldMap.module.css'; export function WorldMap({ diff --git a/src/declaration.d.ts b/src/declaration.d.ts index 986adf27..7dff68b8 100644 --- a/src/declaration.d.ts +++ b/src/declaration.d.ts @@ -1,5 +1,6 @@ +declare module 'bcryptjs'; +declare module 'chartjs-adapter-date-fns'; declare module 'cors'; declare module 'debug'; -declare module 'chartjs-adapter-date-fns'; +declare module 'jsonwebtoken'; declare module 'md5'; -declare module 'request-ip'; diff --git a/src/index.ts b/src/index.ts index 553a44b5..e7b0e6c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,64 +1,64 @@ -export * from 'components/hooks'; +export * from '@/components/hooks'; -export * from 'app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton'; -export * from 'app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm'; -export * from 'app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton'; -export * from 'app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable'; -export * from 'app/(main)/teams/[teamId]/settings/members/TeamMembersTable'; +export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMemberEditButton'; +export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMemberEditForm'; +export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton'; +export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMembersDataTable'; +export * from '@/app/(main)/teams/[teamId]/settings/members/TeamMembersTable'; -export * from 'app/(main)/teams/[teamId]/settings/team/TeamDeleteForm'; -export * from 'app/(main)/teams/[teamId]/settings/team/TeamDetails'; -export * from 'app/(main)/teams/[teamId]/settings/team/TeamEditForm'; -export * from 'app/(main)/teams/[teamId]/settings/team/TeamManage'; +export * from '@/app/(main)/teams/[teamId]/settings/team/TeamDeleteForm'; +export * from '@/app/(main)/teams/[teamId]/settings/team/TeamDetails'; +export * from '@/app/(main)/teams/[teamId]/settings/team/TeamEditForm'; +export * from '@/app/(main)/teams/[teamId]/settings/team/TeamManage'; -export * from 'app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton'; -export * from 'app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable'; -export * from 'app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable'; +export * from '@/app/(main)/teams/[teamId]/settings/websites/TeamWebsiteRemoveButton'; +export * from '@/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesDataTable'; +export * from '@/app/(main)/teams/[teamId]/settings/websites/TeamWebsitesTable'; -export * from 'app/(main)/settings/teams/TeamAddForm'; -export * from 'app/(main)/settings/teams/TeamJoinForm'; -export * from 'app/(main)/settings/teams/TeamLeaveButton'; -export * from 'app/(main)/settings/teams/TeamLeaveForm'; -export * from 'app/(main)/settings/teams/TeamsAddButton'; -export * from 'app/(main)/settings/teams/TeamsDataTable'; -export * from 'app/(main)/settings/teams/TeamsHeader'; -export * from 'app/(main)/settings/teams/TeamsJoinButton'; -export * from 'app/(main)/settings/teams/TeamsTable'; -export * from 'app/(main)/settings/teams/WebsiteTags'; +export * from '@/app/(main)/settings/teams/TeamAddForm'; +export * from '@/app/(main)/settings/teams/TeamJoinForm'; +export * from '@/app/(main)/settings/teams/TeamLeaveButton'; +export * from '@/app/(main)/settings/teams/TeamLeaveForm'; +export * from '@/app/(main)/settings/teams/TeamsAddButton'; +export * from '@/app/(main)/settings/teams/TeamsDataTable'; +export * from '@/app/(main)/settings/teams/TeamsHeader'; +export * from '@/app/(main)/settings/teams/TeamsJoinButton'; +export * from '@/app/(main)/settings/teams/TeamsTable'; +export * from '@/app/(main)/settings/teams/WebsiteTags'; -export * from 'app/(main)/settings/websites/[websiteId]/ShareUrl'; -export * from 'app/(main)/settings/websites/[websiteId]/TrackingCode'; -export * from 'app/(main)/settings/websites/[websiteId]/WebsiteData'; -export * from 'app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm'; -export * from 'app/(main)/settings/websites/[websiteId]/WebsiteEditForm'; -export * from 'app/(main)/settings/websites/[websiteId]/WebsiteResetForm'; -export * from 'app/(main)/settings/websites/[websiteId]/WebsiteSettings'; +export * from '@/app/(main)/settings/websites/[websiteId]/ShareUrl'; +export * from '@/app/(main)/settings/websites/[websiteId]/TrackingCode'; +export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteData'; +export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm'; +export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteEditForm'; +export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteResetForm'; +export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteSettings'; -export * from 'app/(main)/settings/websites/WebsiteAddButton'; -export * from 'app/(main)/settings/websites/WebsiteAddForm'; -export * from 'app/(main)/settings/websites/WebsitesDataTable'; -export * from 'app/(main)/settings/websites/WebsitesHeader'; -export * from 'app/(main)/settings/websites/WebsitesTable'; +export * from '@/app/(main)/settings/websites/WebsiteAddButton'; +export * from '@/app/(main)/settings/websites/WebsiteAddForm'; +export * from '@/app/(main)/settings/websites/WebsitesDataTable'; +export * from '@/app/(main)/settings/websites/WebsitesHeader'; +export * from '@/app/(main)/settings/websites/WebsitesTable'; -export * from 'app/(main)/teams/[teamId]/TeamProvider'; -export * from 'app/(main)/websites/[websiteId]/WebsiteProvider'; +export * from '@/app/(main)/teams/[teamId]/TeamProvider'; +export * from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; -export * from 'components/common/ConfirmationForm'; -export * from 'components/common/DataTable'; -export * from 'components/common/Empty'; -export * from 'components/common/ErrorBoundary'; -export * from 'components/common/ErrorMessage'; -export * from 'components/common/Favicon'; -export * from 'components/common/FilterButtons'; -export * from 'components/common/FilterLink'; -export * from 'components/common/HamburgerButton'; -export * from 'components/common/HoverTooltip'; -export * from 'components/common/LinkButton'; -export * from 'components/common/MobileMenu'; -export * from 'components/common/Pager'; -export * from 'components/common/TypeConfirmationForm'; +export * from '@/components/common/ConfirmationForm'; +export * from '@/components/common/DataTable'; +export * from '@/components/common/Empty'; +export * from '@/components/common/ErrorBoundary'; +export * from '@/components/common/ErrorMessage'; +export * from '@/components/common/Favicon'; +export * from '@/components/common/FilterButtons'; +export * from '@/components/common/FilterLink'; +export * from '@/components/common/HamburgerButton'; +export * from '@/components/common/HoverTooltip'; +export * from '@/components/common/LinkButton'; +export * from '@/components/common/MobileMenu'; +export * from '@/components/common/Pager'; +export * from '@/components/common/TypeConfirmationForm'; -export * from 'components/input/TeamsButton'; -export * from 'components/input/ThemeButton'; +export * from '@/components/input/TeamsButton'; +export * from '@/components/input/ThemeButton'; -export { ROLES } from 'lib/constants'; +export { ROLES } from '@/lib/constants'; diff --git a/src/lang/fa-IR.json b/src/lang/fa-IR.json index d5b48021..493ca487 100644 --- a/src/lang/fa-IR.json +++ b/src/lang/fa-IR.json @@ -1,279 +1,279 @@ { - "label.access-code": "Access code", + "label.access-code": "کد دسترسی", "label.actions": "اقدامات", - "label.activity": "Activity log", - "label.add": "Add", - "label.add-description": "Add description", - "label.add-member": "Add member", - "label.add-step": "Add step", + "label.activity": "فعالیت", + "label.add": "افزودن", + "label.add-description": "افزودن توضیحات", + "label.add-member": "افزودن عضو", + "label.add-step": "افزودن قدم", "label.add-website": "افزودن وب‌سایت", "label.admin": "مدیر", - "label.after": "After", + "label.after": "بعد", "label.all": "همه", - "label.all-time": "همه زمان", - "label.analytics": "Analytics", - "label.average": "Average", - "label.back": "برگشت", - "label.before": "Before", - "label.bounce-rate": "نرخ Bounce", - "label.breakdown": "Breakdown", - "label.browser": "Browser", - "label.browsers": "مروگرها", + "label.all-time": "تمامی زمان‌ها", + "label.analytics": "تجزیه و تحلیل", + "label.average": "میانگین", + "label.back": "بازگشت", + "label.before": "قبل از", + "label.bounce-rate": "نرخ ریزش", + "label.breakdown": "تفکیک", + "label.browser": "مرورگر", + "label.browsers": "مرورگرها", "label.cancel": "انصراف", "label.change-password": "تغییر رمز", - "label.cities": "Cities", - "label.city": "City", - "label.clear-all": "Clear all", - "label.compare": "Compare", - "label.confirm": "Confirm", - "label.confirm-password": "تایید رمز", - "label.contains": "Contains", - "label.continue": "Continue", - "label.count": "Count", + "label.cities": "شهرها", + "label.city": "شهر", + "label.clear-all": "پاک کردن همه", + "label.compare": "مقایسه", + "label.confirm": "تأیید", + "label.confirm-password": "تأیید رمز", + "label.contains": "شامل", + "label.continue": "ادامه", + "label.count": "تعداد", "label.countries": "کشورها", - "label.country": "Country", - "label.create": "Create", - "label.create-report": "Create report", - "label.create-team": "Create team", - "label.create-user": "Create user", - "label.created": "Created", - "label.created-by": "Created By", - "label.current": "Current", + "label.country": "کشور", + "label.create": "ایجاد", + "label.create-report": "ایجاد گزارش", + "label.create-team": "ایجاد تیم", + "label.create-user": "ایجاد کاربر", + "label.created": "ایجاد شد", + "label.created-by": "ایجاد شده توسط", + "label.current": "فعلی", "label.current-password": "رمز فعلی", "label.custom-range": "محدوده‌ی دلخواه", "label.dashboard": "داشبورد", - "label.data": "Data", - "label.date": "Date", + "label.data": "داده", + "label.date": "تاریخ", "label.date-range": "محدوده‌ی تاریخ", - "label.day": "Day", - "label.default-date-range": "محدوده‌ی پیشفرض تاریخ", + "label.day": "روز", + "label.default-date-range": "محدوده‌ی پیش‌فرض تاریخ", "label.delete": "حذف", - "label.delete-report": "Delete report", - "label.delete-team": "Delete team", - "label.delete-user": "Delete user", + "label.delete-report": "حذف گزارش", + "label.delete-team": "حذف تیم", + "label.delete-user": "حذف کاربر", "label.delete-website": "حذف وب‌سایت", - "label.description": "Description", + "label.description": "توضیحات", "label.desktop": "دسکتاپ", - "label.details": "Details", - "label.device": "Device", + "label.details": "جزئیات", + "label.device": "دستگاه", "label.devices": "دستگاه‌ها", "label.dismiss": "رد کردن", - "label.does-not-contain": "Does not contain", + "label.does-not-contain": "شامل نمی‌شود", "label.domain": "دامنه", - "label.dropoff": "Dropoff", + "label.dropoff": "رها کردن", "label.edit": "ویرایش", - "label.edit-dashboard": "Edit dashboard", - "label.edit-member": "Edit member", - "label.enable-share-url": "فعال کردن اشتراک گذاری URL", - "label.end-step": "End Step", - "label.entry": "Entry URL", - "label.event": "Event", - "label.event-data": "Event data", + "label.edit-dashboard": "ویرایش داشبورد", + "label.edit-member": "ویرایش عضو", + "label.enable-share-url": "فعال کردن اشتراک گذاری آدرس اینترنتی", + "label.end-step": "قدم پایانی", + "label.entry": "آدرس اینترنتی ورودی", + "label.event": "رویداد", + "label.event-data": "داده‌های رویداد", "label.events": "رویدادها", - "label.exit": "Exit URL", - "label.false": "False", - "label.field": "Field", - "label.fields": "Fields", - "label.filter": "Filter", + "label.exit": "آدرس اینترنتی خروجی", + "label.false": "نادرست", + "label.field": "فیلد", + "label.fields": "فیلد‌ها", + "label.filter": "فیلتر", "label.filter-combined": "ترکیب شده", "label.filter-raw": "خام", - "label.filters": "Filters", - "label.first-seen": "First seen", - "label.funnel": "Funnel", - "label.funnel-description": "Understand the conversion and drop-off rate of users.", - "label.goal": "Goal", - "label.goals": "Goals", - "label.goals-description": "Track your goals for pageviews and events.", - "label.greater-than": "Greater than", - "label.greater-than-equals": "Greater than or equals", - "label.host": "Host", - "label.hosts": "Hosts", - "label.insights": "Insights", - "label.insights-description": "Dive deeper into your data by using segments and filters.", - "label.is": "Is", - "label.is-not": "Is not", - "label.is-not-set": "Is not set", - "label.is-set": "Is set", - "label.join": "Join", - "label.join-team": "Join team", - "label.journey": "Journey", - "label.journey-description": "Understand how users navigate through your website.", + "label.filters": "فیلترها", + "label.first-seen": "اولین بار دیده شده", + "label.funnel": "فانل", + "label.funnel-description": "نرخ تبدیل و رها کردن کاربران را درک کنید.", + "label.goal": "هدف", + "label.goals": "اهداف", + "label.goals-description": "اهداف خود را برای بازدید از صفحه و رویدادها دنبال کنید.", + "label.greater-than": "بزرگ‌تر از", + "label.greater-than-equals": "بزرگ‌تر یا مساوی", + "label.host": "هاست", + "label.hosts": "هاست‌ها", + "label.insights": "بینش", + "label.insights-description": "با استفاده از بخش‌ها و فیلترها، در داده‌های خود عمیق‌تر شوید.", + "label.is": "برابر است با", + "label.is-not": "برابر نیست با", + "label.is-not-set": "تعیین نشده", + "label.is-set": "تعیین شده", + "label.join": "پیوستن", + "label.join-team": "پیوستن به تیم", + "label.journey": "مسیر", + "label.journey-description": "درک کنید که کاربران چگونه در وب‌سایت شما حرکت می کنند.", "label.language": "زبان", "label.languages": "زبان‌ها", "label.laptop": "لپ‌تاپ", - "label.last-days": "لیست {x} روز گذشته", - "label.last-hours": "لیست {x} ساعت گذشته", - "label.last-months": "Last {x} months", - "label.last-seen": "Last seen", - "label.leave": "Leave", - "label.leave-team": "Leave team", - "label.less-than": "Less than", - "label.less-than-equals": "Less than or equals", + "label.last-days": "{x} روز گذشته", + "label.last-hours": "{x} ساعت گذشته", + "label.last-months": "{x} ماه گذشته", + "label.last-seen": "آخرین بار دیده شده", + "label.leave": "ترک کردن", + "label.leave-team": "ترک تیم", + "label.less-than": "کمتر از", + "label.less-than-equals": "کمتر یا مساوی", "label.login": "ورود", "label.logout": "خروج", - "label.manage": "Manage", - "label.manager": "Manager", - "label.max": "Max", - "label.member": "Member", - "label.members": "Members", - "label.min": "Min", + "label.manage": "مدیریت", + "label.manager": "مدیر", + "label.max": "حداکثر", + "label.member": "عضو", + "label.members": "اعضا", + "label.min": "حداقل", "label.mobile": "موبایل", "label.more": "بیشتر", - "label.my-account": "My account", - "label.my-websites": "My websites", + "label.my-account": "حساب کاربری من", + "label.my-websites": "وب‌سایت‌های من", "label.name": "نام", "label.new-password": "رمز جدید", - "label.none": "None", + "label.none": "هیچ", "label.number-of-records": "{x} {x, plural, one {record} other {records}}", - "label.ok": "OK", - "label.os": "OS", - "label.overview": "Overview", - "label.owner": "ایجاد شده توسط", - "label.page-of": "Page {current} of {total}", + "label.ok": "تایید", + "label.os": "سیستم عامل", + "label.overview": "بررسی کلی", + "label.owner": "مالک", + "label.page-of": "صفحه {current} از {total}", "label.page-views": "بازدید صفحه", - "label.pageTitle": "Page title", + "label.pageTitle": "عنوان صفحه", "label.pages": "صفحه‌ها", "label.password": "رمز", - "label.path": "Path", - "label.paths": "Paths", + "label.path": "مسیر", + "label.paths": "مسیرها", "label.powered-by": "قدرت گرفته توسط {name}", - "label.previous": "Previous", - "label.previous-period": "Previous period", - "label.previous-year": "Previous year", + "label.previous": "قبلی", + "label.previous-period": "دوره‌ی قبل", + "label.previous-year": "سال قبل", "label.profile": "پروفایل", - "label.properties": "Properties", - "label.property": "Property", - "label.queries": "Queries", - "label.query": "Query", - "label.query-parameters": "Query parameters", + "label.properties": "ویژگی‌ها", + "label.property": "ویژگی", + "label.queries": "کوئری‌ها", + "label.query": "کوئری", + "label.query-parameters": "پارامترهای کوئری", "label.realtime": "آمار زنده", - "label.referrer": "Referrer", + "label.referrer": "ارجاع دهنده", "label.referrers": "ارجاع دهندگان", "label.refresh": "به‌روزرسانی", - "label.regenerate": "Regenerate", - "label.region": "Region", - "label.regions": "Regions", - "label.remove": "Remove", - "label.remove-member": "Remove member", - "label.reports": "Reports", + "label.regenerate": "تولید مجدد", + "label.region": "منطقه", + "label.regions": "مناطق", + "label.remove": "حذف", + "label.remove-member": "حذف عضو", + "label.reports": "گزارش‌ها", "label.required": "ضروری", "label.reset": "بازنشانی", - "label.reset-website": "بازنشانی آمار", - "label.retention": "Retention", - "label.retention-description": "Measure your website stickiness by tracking how often users return.", - "label.revenue": "Revenue", - "label.revenue-description": "Look into your revenue across time.", - "label.revenue-property": "Revenue Property", - "label.role": "Role", - "label.run-query": "Run query", + "label.reset-website": "بازنشانی وب‌سایت", + "label.retention": "نرخ بازگشت", + "label.retention-description": "چسبندگی وب‌سایت خود را با دنبال کردن تعداد دفعات بازگشت کاربران اندازه‌گیری کنید.", + "label.revenue": "درآمد", + "label.revenue-description": "به درآمد خود در طول زمان نگاه کنید.", + "label.revenue-property": "ویژگی درآمد", + "label.role": "نقش", + "label.run-query": "اجرای کوئری", "label.save": "ذخیره", - "label.screens": "Screens", - "label.search": "Search", - "label.select": "Select", - "label.select-date": "Select date", - "label.select-role": "Select role", - "label.select-website": "Select website", - "label.session": "Session", - "label.sessions": "Sessions", + "label.screens": "صفحه", + "label.search": "جستجو", + "label.select": "انتخاب", + "label.select-date": "انتخاب تاریخ", + "label.select-role": "انتخاب نقش", + "label.select-website": "انتخاب وب‌سایت", + "label.session": "نشست", + "label.sessions": "نشست‌ها", "label.settings": "تنظیمات", - "label.share-url": "به اشتراک گذاری URL", + "label.share-url": "به اشتراک گذاری آدرس اینترنتی", "label.single-day": "یک روز", - "label.start-step": "Start Step", - "label.steps": "Steps", - "label.sum": "Sum", + "label.start-step": "قدم شروع", + "label.steps": "قدم‌ها", + "label.sum": "جمع", "label.tablet": "تبلت", - "label.team": "Team", - "label.team-id": "Team ID", - "label.team-manager": "Team manager", - "label.team-member": "Team member", - "label.team-name": "Team name", - "label.team-owner": "Team owner", - "label.team-view-only": "Team view only", - "label.team-websites": "Team websites", - "label.teams": "Teams", + "label.team": "تیم", + "label.team-id": "شناسه تیم", + "label.team-manager": "مدیر تیم", + "label.team-member": "عضو تیم", + "label.team-name": "نام تیم", + "label.team-owner": "مالک تیم", + "label.team-view-only": "فقط مشاهده‌ی تیم", + "label.team-websites": "وب‌سایت‌های تیم", + "label.teams": "تیم‌ها", "label.theme": "تم", "label.this-month": "این ماه", "label.this-week": "این هفته", "label.this-year": "امسال", "label.timezone": "منطقه‌ی زمانی", - "label.title": "Title", + "label.title": "عنوان", "label.today": "امروز", - "label.toggle-charts": "Toggle charts", - "label.total": "Total", - "label.total-records": "Total records", + "label.toggle-charts": "نمایش / عدم نمایش نمودارها", + "label.total": "جمع", + "label.total-records": "جمع رکوردها", "label.tracking-code": "کد رهگیری", - "label.transactions": "Transactions", - "label.transfer": "Transfer", - "label.transfer-website": "Transfer website", - "label.true": "True", - "label.type": "Type", - "label.unique": "Unique", + "label.transactions": "تراکنش‌ها", + "label.transfer": "انتقال", + "label.transfer-website": "انتقال وب‌سایت", + "label.true": "درست", + "label.type": "نوع", + "label.unique": "یکتا", "label.unique-visitors": "بازدیدکننده‌های یکتا", - "label.uniqueCustomers": "Unique Customers", + "label.uniqueCustomers": "مشتریان یکتا", "label.unknown": "ناشناخته", - "label.untitled": "Untitled", - "label.update": "Update", - "label.url": "URL", - "label.urls": "URLs", - "label.user": "User", - "label.user-property": "User Property", + "label.untitled": "بدون عنوان", + "label.update": "به‌روزرسانی", + "label.url": "آدرس اینترنتی", + "label.urls": "آدرس‌های اینترنتی", + "label.user": "کاربر", + "label.user-property": "ویژگی کاربر", "label.username": "نام کاربری", - "label.users": "Users", + "label.users": "کاربران", "label.utm": "UTM", - "label.utm-description": "Track your campaigns through UTM parameters.", - "label.value": "Value", - "label.view": "View", + "label.utm-description": "با استفاده از پارامترهای UTM، کمپین‌های خود را بررسی کنید.", + "label.value": "مقدار", + "label.view": "مشاهده", "label.view-details": "مشاهده‌ی جزئیات", - "label.view-only": "View only", + "label.view-only": "فقط مشاهده", "label.views": "بازدید", - "label.views-per-visit": "Views per visit", + "label.views-per-visit": "نمایش‌ها در هر بازدید", "label.visit-duration": "میانگین زمان بازدید", "label.visitors": "بازدیدکننده", - "label.visits": "Visits", - "label.website": "Website", - "label.website-id": "Website ID", + "label.visits": "بازدیدها", + "label.website": "وب‌سایت", + "label.website-id": "شناسه وب‌سایت", "label.websites": "وب‌سایت‌ها", - "label.window": "Window", - "label.yesterday": "Yesterday", - "message.action-confirmation": "Type {confirmation} in the box below to confirm.", - "message.active-users": "{x} هم اکنون {x, plural, one {یک} other {از میان}}", - "message.collected-data": "Collected data", - "message.confirm-delete": "آیا مطمئن هستید می‌خواهید {target} را حذف کنید?", - "message.confirm-leave": "Are you sure you want to leave {target}?", - "message.confirm-remove": "Are you sure you want to remove {target}?", - "message.confirm-reset": "آیا از بازنشانی آمار {target} مطمئن هستید?", - "message.delete-team-warning": "Deleting a team will also delete all team websites.", - "message.delete-website-warning": "همه‌ی داده‌های مرتبط هم حذف خواهد شد.", + "label.window": "پنجره", + "label.yesterday": "دیروز", + "message.action-confirmation": "برای تأیید این عملیات، لطفاً {confirmation} را تایپ کنید.", + "message.active-users": "{x} فعلی {x, plural, one {یک} other {از میان}}", + "message.collected-data": "داده‌های جمع‌آوری شده", + "message.confirm-delete": "آیا مطمئن هستید می‌خواهید {target} را حذف کنید؟", + "message.confirm-leave": "آیا مطمئن هستید می‌خواهید از {target} خارج شوید؟", + "message.confirm-remove": "آیا مطمئن هستید می‌خواهید {target} را حذف کنید؟", + "message.confirm-reset": "آیا مطمئن هستید می‌خواهید {target} را بازنشانی کنید؟", + "message.delete-team-warning": "با حذف تیم، تمامی وب‌سایت‌های تیم هم حذف خواهند شد.", + "message.delete-website-warning": "همه‌ی داده‌های وب‌سایت هم حذف خواهد شد.", "message.error": "مشکلی پیش آمده است.", - "message.event-log": "{event} on {url}", + "message.event-log": "{event} در {url}", "message.go-to-settings": "رفتن به تنظیمات", "message.incorrect-username-password": "نام کاربری / رمز نادرست است.", - "message.invalid-domain": "دامنه‌ی نامعتبر", - "message.min-password-length": "Minimum length of {n} characters", - "message.new-version-available": "A new version of Umami {version} is available!", + "message.invalid-domain": "دامنه نامعتبر است.", + "message.min-password-length": "حداقل طول {n} کاراکتر است.", + "message.new-version-available": "نسخه‌ی جدیدی از Umami {version} در دسترس است.", "message.no-data-available": "اطلاعاتی موجود نیست.", - "message.no-event-data": "No event data is available.", + "message.no-event-data": "هیچ داده‌ای برای این رویداد وجود ندارد.", "message.no-match-password": "رمزها یکسان نیستند", - "message.no-results-found": "No results were found.", - "message.no-team-websites": "This team does not have any websites.", - "message.no-teams": "You have not created any teams.", - "message.no-users": "There are no users.", + "message.no-results-found": "نتیجه‌ای یافت نشد.", + "message.no-team-websites": "هیچ وب‌سایتی برای این تیم وجود ندارد.", + "message.no-teams": "شما هیچ تیمی را ایجاد نکرده‌اید.", + "message.no-users": "هیچ کاربری وجود ندارد.", "message.no-websites-configured": "شما هیچ وب‌سایتی را پیکربندی نکرده‌اید.", "message.page-not-found": "صفحه یافت نشد.", - "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", - "message.reset-website-warning": "تمامی آمارهای این وب‌سایت حذف خواهد شد اما tracking code بدون تغییر باقی می‌ماند.", - "message.saved": "با موفقیت ذخیره شد.", - "message.share-url": "این URL به اشتراک گذاشته شده عمومی برای {target} است.", - "message.team-already-member": "You are already a member of the team.", - "message.team-not-found": "Team not found.", - "message.team-websites-info": "Websites can be viewed by anyone on the team.", + "message.reset-website": "برای بازنشانی وب‌سایت، لطفاً {confirmation} را تایپ کنید.", + "message.reset-website-warning": "تمامی آمارهای این وب‌سایت حذف خواهد شد اما کدهای رهگیری بدون تغییر باقی می‌ماند.", + "message.saved": "ذخیره شد.", + "message.share-url": "آمار وب‌سایت شما به صورت عمومی در آدرس زیر قابل مشاهده است.", + "message.team-already-member": "شما از قبل عضو این تیم هستید.", + "message.team-not-found": "تیم یافت نشد.", + "message.team-websites-info": "وب‌سایت‌ها توسط تمامی اعضای تیم قابل مشاهده هستند.", "message.tracking-code": "کد رهگیری", - "message.transfer-team-website-to-user": "Transfer this website to your account?", - "message.transfer-user-website-to-team": "Select the team to transfer this website to.", - "message.transfer-website": "Transfer website ownership to your account or another team.", - "message.triggered-event": "Triggered event", - "message.user-deleted": "User deleted.", - "message.viewed-page": "Viewed page", + "message.transfer-team-website-to-user": "آیا می‌خواهید این وب‌سایت را به حساب خود منتقل کنید؟", + "message.transfer-user-website-to-team": "تیم مورد نظر را برای انتقال وب‌سایت انتخاب کنید.", + "message.transfer-website": "مالکیت وب‌سایت را به حساب خودت یا یک تیم دیگر منتقل کنید.", + "message.triggered-event": "رویداد فعال شده", + "message.user-deleted": "کاربر حذف شد.", + "message.viewed-page": "صفحه مشاهده شد", "message.visitor-log": "بازدیدکننده از کشور {country} با مروگر {browser} در {os} {device}", - "message.visitors-dropped-off": "Visitors dropped off" + "message.visitors-dropped-off": "ریزش بازدیدکننده‌ها" } diff --git a/src/lang/ga-ES.json b/src/lang/ga-ES.json index 14b9a474..3af959f7 100644 --- a/src/lang/ga-ES.json +++ b/src/lang/ga-ES.json @@ -1,279 +1,279 @@ { - "label.access-code": "Access code", + "label.access-code": "Código de acceso", "label.actions": "Accións", - "label.activity": "Activity log", - "label.add": "Add", - "label.add-description": "Add description", - "label.add-member": "Add member", - "label.add-step": "Add step", + "label.activity": "Rexistro de actividade", + "label.add": "Engadir", + "label.add-description": "Engadir descrición", + "label.add-member": "Engadir membro", + "label.add-step": "Engadir paso", "label.add-website": "Engadir sitio web", - "label.admin": "Administradora", + "label.admin": "Administrador/a", "label.after": "After", "label.all": "Todo", "label.all-time": "Sempre", - "label.analytics": "Analytics", - "label.average": "Average", + "label.analytics": "Analíticas", + "label.average": "Media", "label.back": "Atrás", - "label.before": "Before", + "label.before": "Antes", "label.bounce-rate": "Proporción de rebote", - "label.breakdown": "Breakdown", - "label.browser": "Browser", + "label.breakdown": "Desglose", + "label.browser": "Navegador", "label.browsers": "Navegadores", "label.cancel": "Cancelar", "label.change-password": "Mudar contrasinal", - "label.cities": "Cities", - "label.city": "City", + "label.cities": "Cidades", + "label.city": "Cidade", "label.clear-all": "Clear all", - "label.compare": "Compare", - "label.confirm": "Confirm", + "label.compare": "Comparar", + "label.confirm": "Confirmar", "label.confirm-password": "Confirmar contrasinal", - "label.contains": "Contains", - "label.continue": "Continue", - "label.count": "Count", + "label.contains": "Contén", + "label.continue": "Continuar", + "label.count": "Reconto", "label.countries": "Países", - "label.country": "Country", - "label.create": "Create", - "label.create-report": "Create report", - "label.create-team": "Create team", - "label.create-user": "Create user", - "label.created": "Created", - "label.created-by": "Created By", - "label.current": "Current", + "label.country": "País", + "label.create": "Crear", + "label.create-report": "Crear report", + "label.create-team": "Crear team", + "label.create-user": "Crear user", + "label.created": "Creado", + "label.created-by": "Creado por", + "label.current": "Actual", "label.current-password": "Contrasinal actual", "label.custom-range": "Rango personalizado", "label.dashboard": "Taboleiro", - "label.data": "Data", - "label.date": "Date", + "label.data": "Datos", + "label.date": "Data", "label.date-range": "Rango temporal", - "label.day": "Day", + "label.day": "Día", "label.default-date-range": "Rango temporal por defecto", "label.delete": "Eliminar", - "label.delete-report": "Delete report", - "label.delete-team": "Delete team", - "label.delete-user": "Delete user", + "label.delete-report": "Eliminar reporte", + "label.delete-team": "Eliminar equipo", + "label.delete-user": "Eliminar usuario", "label.delete-website": "Eliminar sitio web", - "label.description": "Description", + "label.description": "Descripción", "label.desktop": "Escritorio", - "label.details": "Details", - "label.device": "Device", + "label.details": "Detalles", + "label.device": "Dispositivo", "label.devices": "Dispositivos", "label.dismiss": "Desbotar", - "label.does-not-contain": "Does not contain", + "label.does-not-contain": "Non contén", "label.domain": "Dominio", - "label.dropoff": "Dropoff", + "label.dropoff": "Disminución", "label.edit": "Editar", - "label.edit-dashboard": "Edit dashboard", - "label.edit-member": "Edit member", + "label.edit-dashboard": "Editar taboleiro", + "label.edit-member": "Editar membro", "label.enable-share-url": "Activar URL de compartición", "label.end-step": "End Step", "label.entry": "Entry URL", - "label.event": "Event", - "label.event-data": "Event data", + "label.event": "Evento", + "label.event-data": "Datos do evento", "label.events": "Eventos", - "label.exit": "Exit URL", - "label.false": "False", - "label.field": "Field", - "label.fields": "Fields", - "label.filter": "Filter", + "label.exit": "URL de saída", + "label.false": "Falso", + "label.field": "Campo", + "label.fields": "Campos", + "label.filter": "Filtro", "label.filter-combined": "Combinado", - "label.filter-raw": "Raw", - "label.filters": "Filters", - "label.first-seen": "First seen", + "label.filter-raw": "Crú", + "label.filters": "Filtros", + "label.first-seen": "Primeira visita", "label.funnel": "Funnel", - "label.funnel-description": "Understand the conversion and drop-off rate of users.", - "label.goal": "Goal", - "label.goals": "Goals", - "label.goals-description": "Track your goals for pageviews and events.", - "label.greater-than": "Greater than", - "label.greater-than-equals": "Greater than or equals", - "label.host": "Host", - "label.hosts": "Hosts", + "label.funnel-description": "Entende a taxa de conversión e de abandono dos usuarios.", + "label.goal": "Obxectivo", + "label.goals": "Obxectivos", + "label.goals-description": "Segue os teus obxectivos de visualizacións de páxinas e eventos.", + "label.greater-than": "Maior que", + "label.greater-than-equals": "Maior ou igual que", + "label.host": "Dominio", + "label.hosts": "Dominios", "label.insights": "Insights", "label.insights-description": "Dive deeper into your data by using segments and filters.", - "label.is": "Is", - "label.is-not": "Is not", - "label.is-not-set": "Is not set", - "label.is-set": "Is set", - "label.join": "Join", - "label.join-team": "Join team", - "label.journey": "Journey", - "label.journey-description": "Understand how users navigate through your website.", + "label.is": "É", + "label.is-not": "Non é", + "label.is-not-set": "Non está establecido", + "label.is-set": "Está establecido", + "label.join": "Unirse", + "label.join-team": "Unirse ao equipo", + "label.journey": "Traxectoria", + "label.journey-description": "Entende como os usuarios navegan polo teu sitio web.", "label.language": "Idioma", "label.languages": "Idiomas", "label.laptop": "Portátil", "label.last-days": "Últimos {x} días", "label.last-hours": "Últimas {x} horas", - "label.last-months": "Last {x} months", - "label.last-seen": "Last seen", - "label.leave": "Leave", - "label.leave-team": "Leave team", - "label.less-than": "Less than", - "label.less-than-equals": "Less than or equals", + "label.last-months": "Últimos {x} meses", + "label.last-seen": "Última visita", + "label.leave": "Deixar", + "label.leave-team": "Deixar o equipo", + "label.less-than": "Menor que", + "label.less-than-equals": "Menor ou igual que", "label.login": "Acceder", "label.logout": "Pechar sesión", - "label.manage": "Manage", - "label.manager": "Manager", + "label.manage": "Xestionar", + "label.manager": "Xestor", "label.max": "Max", - "label.member": "Member", - "label.members": "Members", + "label.member": "Membro", + "label.members": "Membros", "label.min": "Min", "label.mobile": "Móbil", "label.more": "Máis", - "label.my-account": "My account", - "label.my-websites": "My websites", + "label.my-account": "A miña conta", + "label.my-websites": "Os meus sitios web", "label.name": "Nome", "label.new-password": "Novo contrasinal", "label.none": "None", "label.number-of-records": "{x} {x, plural, one {record} other {records}}", "label.ok": "OK", - "label.os": "OS", - "label.overview": "Overview", - "label.owner": "Dona", - "label.page-of": "Page {current} of {total}", + "label.os": "Sistema operativo", + "label.overview": "Resumo", + "label.owner": "Propietario/a", + "label.page-of": "Páxina {current} de {total}", "label.page-views": "Vistas de páxinas", - "label.pageTitle": "Page title", + "label.pageTitle": "Título da páxina", "label.pages": "Páxinas", "label.password": "Contrasinal", - "label.path": "Path", - "label.paths": "Paths", + "label.path": "Ruta", + "label.paths": "Rutas", "label.powered-by": "Funciona grazas a {name}", - "label.previous": "Previous", - "label.previous-period": "Previous period", - "label.previous-year": "Previous year", + "label.previous": "Anterior", + "label.previous-period": "Periodo anterior", + "label.previous-year": "Ano anterior", "label.profile": "Perfil", - "label.properties": "Properties", - "label.property": "Property", - "label.queries": "Queries", - "label.query": "Query", - "label.query-parameters": "Query parameters", + "label.properties": "Propiedades", + "label.property": "Propiedade", + "label.queries": "Peticións", + "label.query": "Petición", + "label.query-parameters": "Parámetros da petición", "label.realtime": "Agora mesmo", - "label.referrer": "Referrer", + "label.referrer": "Orixe", "label.referrers": "Orixes", "label.refresh": "Actualizar", - "label.regenerate": "Regenerate", - "label.region": "Region", - "label.regions": "Regions", - "label.remove": "Remove", - "label.remove-member": "Remove member", - "label.reports": "Reports", + "label.regenerate": "Rexenerar", + "label.region": "Rexión", + "label.regions": "Rexións", + "label.remove": "Eliminar", + "label.remove-member": "Eliminar membro", + "label.reports": "Reportes", "label.required": "Requerido", "label.reset": "Restablecer", - "label.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", - "label.retention": "Retention", - "label.retention-description": "Measure your website stickiness by tracking how often users return.", - "label.revenue": "Revenue", - "label.revenue-description": "Look into your revenue across time.", + "label.reset-website": "Para restablecer este sitio web, escriba {confirmation} na caixa de texto de embaixo para confirmar.", + "label.retention": "Retención", + "label.retention-description": "Mide a fidelidade dos usuarios ao teu sitio web seguindo a frecuencia coa que volven.", + "label.revenue": "Ingresos", + "label.revenue-description": "Consulta os teus ingresos ao longo do tempo.", "label.revenue-property": "Revenue Property", - "label.role": "Role", - "label.run-query": "Run query", + "label.role": "Rol", + "label.run-query": "Executar petición", "label.save": "Gardar", - "label.screens": "Screens", - "label.search": "Search", - "label.select": "Select", - "label.select-date": "Select date", - "label.select-role": "Select role", - "label.select-website": "Select website", - "label.session": "Session", - "label.sessions": "Sessions", + "label.screens": "Pantallas", + "label.search": "Buscar", + "label.select": "Seleccionar", + "label.select-date": "Seleccionar data", + "label.select-role": "Seleccionar rol", + "label.select-website": "Seleccionar sitio web", + "label.session": "Sesión", + "label.sessions": "Sesións", "label.settings": "Axustes", "label.share-url": "Compartir URL", "label.single-day": "Un só día", "label.start-step": "Start Step", - "label.steps": "Steps", - "label.sum": "Sum", + "label.steps": "Pasos", + "label.sum": "Suma", "label.tablet": "Tableta", - "label.team": "Team", - "label.team-id": "Team ID", - "label.team-manager": "Team manager", - "label.team-member": "Team member", - "label.team-name": "Team name", - "label.team-owner": "Team owner", - "label.team-view-only": "Team view only", - "label.team-websites": "Team websites", - "label.teams": "Teams", + "label.team": "Equipo", + "label.team-id": "ID do equipo", + "label.team-manager": "Xestor do equipo", + "label.team-member": "Membro do equipo", + "label.team-name": "Nome do equipo", + "label.team-owner": "Propietario do equipo", + "label.team-view-only": "Equipo de só lectura", + "label.team-websites": "Sitios web do equipo", + "label.teams": "Equipos", "label.theme": "Decorado", "label.this-month": "Este mes", "label.this-week": "Esta semana", "label.this-year": "Este ano", "label.timezone": "Zona horaria", - "label.title": "Title", + "label.title": "Título", "label.today": "Hoxe", "label.toggle-charts": "Activación das gráficas", "label.total": "Total", - "label.total-records": "Total records", - "label.tracking-code": "Código de seguimento", - "label.transactions": "Transactions", - "label.transfer": "Transfer", - "label.transfer-website": "Transfer website", - "label.true": "True", - "label.type": "Type", - "label.unique": "Unique", + "label.total-records": "Rexistros totais", + "label.tracking-code": "Código de seguemento", + "label.transactions": "Transaccións", + "label.transfer": "Transferir", + "label.transfer-website": "Transferir sitio web", + "label.true": "Verdadeiro", + "label.type": "Tipo", + "label.unique": "Único", "label.unique-visitors": "Visitas únicas", - "label.uniqueCustomers": "Unique Customers", + "label.uniqueCustomers": "Clientes únicos", "label.unknown": "Descoñecido", - "label.untitled": "Untitled", - "label.update": "Update", + "label.untitled": "Sen título", + "label.update": "Actualizar", "label.url": "URL", "label.urls": "URLs", - "label.user": "User", - "label.user-property": "User Property", + "label.user": "Usuario", + "label.user-property": "Propiedade do usuario", "label.username": "Identificador", - "label.users": "Users", + "label.users": "Usuarios", "label.utm": "UTM", - "label.utm-description": "Track your campaigns through UTM parameters.", - "label.value": "Value", - "label.view": "View", + "label.utm-description": "Segue as túas campañas a través dos parámetros UTM.", + "label.value": "Valor", + "label.view": "Vista", "label.view-details": "Ver detalles", - "label.view-only": "View only", + "label.view-only": "Só lectura", "label.views": "Visualizacións", - "label.views-per-visit": "Views per visit", + "label.views-per-visit": "Visualizacións por visita", "label.visit-duration": "Tempo medio de visita", "label.visitors": "Visitantes", - "label.visits": "Visits", - "label.website": "Website", - "label.website-id": "Website ID", + "label.visits": "Visitas", + "label.website": "Sitio web", + "label.website-id": "ID do sitio web", "label.websites": "Sitios web", - "label.window": "Window", - "label.yesterday": "Yesterday", - "message.action-confirmation": "Type {confirmation} in the box below to confirm.", + "label.window": "Ventá", + "label.yesterday": "Onte", + "message.action-confirmation": "Escribe {confirmation} na caixa de embaixo para confirmar.", "message.active-users": "{x} actual {x, plural, one {visitante} other {visitantes}}", - "message.collected-data": "Collected data", - "message.confirm-delete": "Tes a certeza de querer eliminar {target}?", - "message.confirm-leave": "Are you sure you want to leave {target}?", - "message.confirm-remove": "Are you sure you want to remove {target}?", - "message.confirm-reset": "Tes a certeza de querer restablecer as estatísticas de {target}?", - "message.delete-team-warning": "Deleting a team will also delete all team websites.", + "message.collected-data": "Datos recopilados", + "message.confirm-delete": "Estás seguro/a de que queres eliminar {target}?", + "message.confirm-leave": "Estás seguro/a de que queres deixar {target}?", + "message.confirm-remove": "Estás seguro/a de que queres eliminar {target}?", + "message.confirm-reset": "Estás seguro/a de querer restablecer as estatísticas de {target}?", + "message.delete-team-warning": "Eliminar un equipo tamén eliminará tódolos sitios web do equipo.", "message.delete-website-warning": "Tamén serán borrados tódolos datos asociados.", "message.error": "Houbo un fallo.", - "message.event-log": "{event} on {url}", + "message.event-log": "{event} en {url}", "message.go-to-settings": "Ir aos axustes", "message.incorrect-username-password": "Credenciais incorrectas.", "message.invalid-domain": "Dominio non válido", - "message.min-password-length": "Minimum length of {n} characters", - "message.new-version-available": "A new version of Umami {version} is available!", + "message.min-password-length": "Lonxitude mínima de {n} caracteres", + "message.new-version-available": "Unha nova versión de Umami {version} está dispoñible!", "message.no-data-available": "Sen datos dispoñibles.", - "message.no-event-data": "No event data is available.", + "message.no-event-data": "Sen datos de eventos dispoñibles.", "message.no-match-password": "Non concordan os contrasinais", - "message.no-results-found": "No results were found.", - "message.no-team-websites": "This team does not have any websites.", - "message.no-teams": "You have not created any teams.", - "message.no-users": "There are no users.", + "message.no-results-found": "Non se atoparon resultados.", + "message.no-team-websites": "Este equipo non ten ningún sitio web.", + "message.no-teams": "Non creaches ningún equipo.", + "message.no-users": "Non hai usuarios.", "message.no-websites-configured": "Non tes sitios web configurados.", "message.page-not-found": "Páxina non atopada.", - "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", + "message.reset-website": "Para restablecer este sitio web, escriba {confirmation} na caixa de embaixo para confirmar.", "message.reset-website-warning": "Vanse eliminar tódalas estatísticas deste sitio web, pero o código de seguimento permanecerá sen cambios.", "message.saved": "Gardouse correctamente.", "message.share-url": "Este é o URL da compartición pública de {target}.", - "message.team-already-member": "You are already a member of the team.", - "message.team-not-found": "Team not found.", - "message.team-websites-info": "Websites can be viewed by anyone on the team.", + "message.team-already-member": "Xa es membro do equipo.", + "message.team-not-found": "Equipo non atopado.", + "message.team-websites-info": "Os sitios web poden ser vistos por calquera membro do equipo.", "message.tracking-code": "Código de seguimento", - "message.transfer-team-website-to-user": "Transfer this website to your account?", - "message.transfer-user-website-to-team": "Select the team to transfer this website to.", - "message.transfer-website": "Transfer website ownership to your account or another team.", - "message.triggered-event": "Triggered event", - "message.user-deleted": "User deleted.", - "message.viewed-page": "Viewed page", + "message.transfer-team-website-to-user": "Transferir este sitio web á túa conta?", + "message.transfer-user-website-to-team": "Selecciona o equipo ao que transferir este sitio web.", + "message.transfer-website": "Transferir propiedade do sitio web á túa conta ou a outro equipo.", + "message.triggered-event": "Activou o evento", + "message.user-deleted": "Usuario eliminado.", + "message.viewed-page": "Páxina vista", "message.visitor-log": "Visitante desde {country} usando {browser} en {os} {device}", - "message.visitors-dropped-off": "Visitors dropped off" + "message.visitors-dropped-off": "Visitantes abandonados" } diff --git a/src/lang/mn-MN.json b/src/lang/mn-MN.json index 769c58a5..60797aef 100644 --- a/src/lang/mn-MN.json +++ b/src/lang/mn-MN.json @@ -4,14 +4,14 @@ "label.activity": "Үйл ажиллагааны бүртгэл", "label.add": "Нэмэх", "label.add-description": "Тайлбар нэмэх", - "label.add-member": "Add member", - "label.add-step": "Add step", + "label.add-member": "Гишүүн нэмэх", + "label.add-step": "Алхам нэмэх", "label.add-website": "Веб нэмэх", "label.admin": "Админ", "label.after": "Хойно", "label.all": "Бүх", "label.all-time": "Бүх цаг үеийн", - "label.analytics": "Analytics", + "label.analytics": "Аналитик", "label.average": "Дундаж", "label.back": "Буцах", "label.before": "Өмнө", @@ -24,12 +24,12 @@ "label.cities": "Хотууд", "label.city": "Хот", "label.clear-all": "Бүгдийг арилгах", - "label.compare": "Compare", + "label.compare": "Харьцуулах", "label.confirm": "Батлах", "label.confirm-password": "Шинэ нууц үгээ давтах", "label.contains": "Агуулах", "label.continue": "Үргэлжлүүлэх", - "label.count": "Count", + "label.count": "Тоо", "label.countries": "Улс", "label.country": "Улс", "label.create": "Үүсгэх", @@ -37,8 +37,8 @@ "label.create-team": "Баг үүсгэх", "label.create-user": "Хэрэглэгч үүсгэх", "label.created": "Үүсгэсэн", - "label.created-by": "Created By", - "label.current": "Current", + "label.created-by": "Үүсгэсэн", + "label.current": "Одоогийн", "label.current-password": "Ашиглаж буй нууц үг", "label.custom-range": "Дурын хугацаа", "label.dashboard": "Хянах самбар", @@ -48,7 +48,7 @@ "label.day": "Өдөр", "label.default-date-range": "Өгөгдмөл хугацааны муж", "label.delete": "Устгах", - "label.delete-report": "Delete report", + "label.delete-report": "Тайлан устгах", "label.delete-team": "Баг устгах", "label.delete-user": "Хэрэглэгч устгах", "label.delete-website": "Веб устгах", @@ -63,14 +63,14 @@ "label.dropoff": "Уналт", "label.edit": "Засах", "label.edit-dashboard": "Хянах самбар засах", - "label.edit-member": "Edit member", + "label.edit-member": "Гишүүн засах", "label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх", - "label.end-step": "End Step", - "label.entry": "Entry URL", + "label.end-step": "Төгсгөлийн алхам", + "label.entry": "Орох зам", "label.event": "Үйлдэл", "label.event-data": "Үйлдлийн өгөгдөл", "label.events": "Үйлдэл", - "label.exit": "Exit URL", + "label.exit": "Гарах зам", "label.false": "Худал", "label.field": "Талбар", "label.fields": "Талбар", @@ -78,16 +78,16 @@ "label.filter-combined": "Нэгтгэсэн", "label.filter-raw": "Түүхий", "label.filters": "Шүүлтүүр", - "label.first-seen": "First seen", + "label.first-seen": "Анх харсан", "label.funnel": "Цутгал", "label.funnel-description": "Хэрэглэгчдийн шилжилт, уналтын хэмжээг шинжлэх.", - "label.goal": "Goal", - "label.goals": "Goals", - "label.goals-description": "Track your goals for pageviews and events.", + "label.goal": "Зорилго", + "label.goals": "Зорилго", + "label.goals-description": "Хуудас үзсэн болон үйлдлийн зорилгыг мөрдөх.", "label.greater-than": "Их", "label.greater-than-equals": "Их буюу тэнцүү", - "label.host": "Host", - "label.hosts": "Hosts", + "label.host": "Хост", + "label.hosts": "Хост", "label.insights": "Шинжлэх", "label.insights-description": "Өгөгдлөө хэсэгчлэн хуваах, шүүх байдлаар задлан шинжлэх.", "label.is": "Бол", @@ -96,36 +96,36 @@ "label.is-set": "Утга оноосон", "label.join": "Нэгдэх", "label.join-team": "Багт нэгдэх", - "label.journey": "Journey", - "label.journey-description": "Understand how users navigate through your website.", + "label.journey": "Аялал", + "label.journey-description": "Хэрэглэгчид таны цахим хуудсаар хэрхэн шилжиж явсныг шинжлэх.", "label.language": "Хэл", "label.languages": "Хэл", "label.laptop": "Зөөврийн компьютер", "label.last-days": "Сүүлийн {x} хоног", "label.last-hours": "Сүүлийн {x} цаг", - "label.last-months": "Last {x} months", - "label.last-seen": "Last seen", + "label.last-months": "Сүүлийн {x} сар", + "label.last-seen": "Сүүлд харагдсан", "label.leave": "Гарах", "label.leave-team": "Багаас гарах", "label.less-than": "Бага", "label.less-than-equals": "Бага буюу тэнцүү", "label.login": "Нэвтрэх", "label.logout": "Гарах", - "label.manage": "Manage", - "label.manager": "Manager", + "label.manage": "Удирдах", + "label.manager": "Удирдагч", "label.max": "Max", - "label.member": "Member", + "label.member": "Гишүүн", "label.members": "Гишүүд", "label.min": "Min", "label.mobile": "Утас", "label.more": "Цааш", - "label.my-account": "My account", + "label.my-account": "Миний бүртгэл", "label.my-websites": "Миний вебүүд", "label.name": "Нэр", "label.new-password": "Шинэ нууц үг", "label.none": "Байхгүй", - "label.number-of-records": "{x} {x, plural, one {record} other {records}}", - "label.ok": "OK", + "label.number-of-records": "{x} {x, plural, one {бичлэг} other {бичлэг}}", + "label.ok": "ЗА", "label.os": "OS", "label.overview": "Тойм", "label.owner": "Эзэмшигч", @@ -134,15 +134,15 @@ "label.pageTitle": "Хуудасны гарчиг", "label.pages": "Хуудас", "label.password": "Нууц үг", - "label.path": "Path", - "label.paths": "Paths", + "label.path": "Зам", + "label.paths": "Зам", "label.powered-by": "{name} дээр суурилсан", - "label.previous": "Previous", - "label.previous-period": "Previous period", - "label.previous-year": "Previous year", + "label.previous": "Өмнөх", + "label.previous-period": "Өмнөх үе", + "label.previous-year": "Өмнөх жил", "label.profile": "Бүртгэл", - "label.properties": "Properties", - "label.property": "Property", + "label.properties": "Шинж чанар", + "label.property": "Шинж чанар", "label.queries": "Query-нүүд", "label.query": "Query", "label.query-parameters": "Query параметр", @@ -154,22 +154,22 @@ "label.region": "Бүс", "label.regions": "Бүсүүд", "label.remove": "Устгах", - "label.remove-member": "Remove member", + "label.remove-member": "Гишүүн хасах", "label.reports": "Тайлан", "label.required": "Шаардлагатай", "label.reset": "Дахин эхлүүлэх", "label.reset-website": "Тоон үзүүлэлтийг дахин эхлүүлэх", "label.retention": "Барилт", "label.retention-description": "Хэрэглэгчид таны веб рүү дахин хандах буюу хэрэглэгчдээ хэр тогтоож буйг хэмжих.", - "label.revenue": "Revenue", - "label.revenue-description": "Look into your revenue across time.", - "label.revenue-property": "Revenue Property", + "label.revenue": "Орлого", + "label.revenue-description": "Цаг хугацааны туршид орлогын өөрчлөлтийг харах.", + "label.revenue-property": "Орлогын шинж чанар", "label.role": "Эрх", "label.run-query": "Query ажиллуулах", "label.save": "Хадгалах", "label.screens": "Дэлгэц", "label.search": "Хайх", - "label.select": "Select", + "label.select": "Сонгох", "label.select-date": "Огноо сонгох", "label.select-role": "Select role", "label.select-website": "Веб сонгох", @@ -178,13 +178,13 @@ "label.settings": "Тохиргоо", "label.share-url": "Хуваалцах холбоос", "label.single-day": "Нэг өдөр", - "label.start-step": "Start Step", - "label.steps": "Steps", + "label.start-step": "Эхлэх алхам", + "label.steps": "Алхам", "label.sum": "Нийлбэр", "label.tablet": "Таблет", "label.team": "Баг", "label.team-id": "Багийн ID", - "label.team-manager": "Team manager", + "label.team-manager": "Багийн удирдагч", "label.team-member": "Багийн гишүүн", "label.team-name": "Багийн нэр", "label.team-owner": "Багийн эзэмшигч", @@ -203,46 +203,46 @@ "label.total-records": "Нийт мөрийн тоо", "label.tracking-code": "Мөрдөх код", "label.transactions": "Transactions", - "label.transfer": "Transfer", - "label.transfer-website": "Transfer website", + "label.transfer": "Шилжүүлэх", + "label.transfer-website": "Вебийг шилжүүлэх", "label.true": "Үнэн", "label.type": "Төрөл", "label.unique": "Давхардаагүй", "label.unique-visitors": "Зочин", - "label.uniqueCustomers": "Unique Customers", + "label.uniqueCustomers": "Давтагдаагүй зочин", "label.unknown": "Тодорхойгүй", "label.untitled": "Гарчиггүй", - "label.update": "Update", + "label.update": "Шинэчлэх", "label.url": "URL", - "label.urls": "URLs", + "label.urls": "URL-ууд", "label.user": "Хэрэглэгч", - "label.user-property": "User Property", + "label.user-property": "Хэрэглэгчийн шинж", "label.username": "Хэрэглэгчийн нэр", "label.users": "Хэрэглэгчид", "label.utm": "UTM", - "label.utm-description": "Track your campaigns through UTM parameters.", + "label.utm-description": "UTM параметраар кампанит ажлаа мөрдөх.", "label.value": "Утга", "label.view": "Харах", "label.view-details": "Дэлгэрүүлж харах", "label.view-only": "Зөвхөн үзэх", "label.views": "Үзсэн", - "label.views-per-visit": "Views per visit", + "label.views-per-visit": "Зочдын хуудас үзсэн тоо", "label.visit-duration": "Зочилсон дундаж хугацаа", "label.visitors": "Зочин", - "label.visits": "Visits", + "label.visits": "Зочилсон", "label.website": "Веб", "label.website-id": "Вебийн ID", "label.websites": "Вебүүд", "label.window": "Цонх", "label.yesterday": "Өчигдөр", - "message.action-confirmation": "Type {confirmation} in the box below to confirm.", + "message.action-confirmation": "Доорх хэсэгт {confirmation} гэж бичин баталгаажуулна уу.", "message.active-users": "одоо {x} {x, plural, one {зочин} other {зочин}} байна", - "message.collected-data": "Collected data", + "message.collected-data": "Цуглуулсан өгөгдөл", "message.confirm-delete": "Та {target}-г устгахдаа итгэлтэй байна уу?", "message.confirm-leave": "Та {target}-с гарахдаа итгэлтэй байна уу?", - "message.confirm-remove": "Are you sure you want to remove {target}?", + "message.confirm-remove": "Та {target}-г устгахдаа итгэлтэй байна уу?", "message.confirm-reset": "Та {target}-н тоон үзүүлэлтүүдийг устгахдаа итгэлтэй байна уу?", - "message.delete-team-warning": "Deleting a team will also delete all team websites.", + "message.delete-team-warning": "Баг устгах нь мөн түүнд харъяалагдах вебүүдийг устгах болно.", "message.delete-website-warning": "Энэ вебтэй холбоотой бүх өгөгдөл устах болно.", "message.error": "Ямар нэг зүйл буруу боллоо.", "message.event-log": "{url}-д {event}", @@ -268,12 +268,12 @@ "message.team-not-found": "Баг олдсонгүй.", "message.team-websites-info": "Вебийг багийн бүх гишүүд үзэж болно.", "message.tracking-code": "Энэ вебийн хандалтуудыг мөрдөхийн тулд доорх кодыг HTML-нхээ ... хэсэгт байрлуулна уу.", - "message.transfer-team-website-to-user": "Transfer this website to your account?", - "message.transfer-user-website-to-team": "Select the team to transfer this website to.", - "message.transfer-website": "Transfer website ownership to your account or another team.", - "message.triggered-event": "Triggered event", + "message.transfer-team-website-to-user": "Энэ вебийг өөрийн бүртгэл рүү шилжүүлэх үү?", + "message.transfer-user-website-to-team": "Энэ вебийг шилжүүлж авах багийг сонгоно уу.", + "message.transfer-website": "Энэ вебийг өөрийн бүртгэл рүү эсвэл багт шилжүүлж авах.", + "message.triggered-event": "Өдөөсөн үйлдэл", "message.user-deleted": "Хэрэглэгч устсан.", - "message.viewed-page": "Viewed page", + "message.viewed-page": "Үзсэн хуудас", "message.visitor-log": "{country} улсаас {os} {device} дээр {browser} хөтөч ашиглан орсон", - "message.visitors-dropped-off": "Visitors dropped off" + "message.visitors-dropped-off": "Зочдын уналт" } diff --git a/src/lib/__tests__/detect.test.ts b/src/lib/__tests__/detect.test.ts index 14c67dde..1cb558ad 100644 --- a/src/lib/__tests__/detect.test.ts +++ b/src/lib/__tests__/detect.test.ts @@ -6,17 +6,17 @@ const IP = '127.0.0.1'; test('getIpAddress: Custom header', () => { process.env.CLIENT_IP_HEADER = 'x-custom-ip-header'; - expect(detect.getIpAddress({ headers: { 'x-custom-ip-header': IP } } as any)).toEqual(IP); + expect(detect.getIpAddress(new Headers({ 'x-custom-ip-header': IP }))).toEqual(IP); }); test('getIpAddress: CloudFlare header', () => { - expect(detect.getIpAddress({ headers: { 'cf-connecting-ip': IP } } as any)).toEqual(IP); + expect(detect.getIpAddress(new Headers({ 'cf-connecting-ip': IP }))).toEqual(IP); }); test('getIpAddress: Standard header', () => { - expect(detect.getIpAddress({ headers: { 'x-forwarded-for': IP } } as any)).toEqual(IP); + expect(detect.getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP); }); test('getIpAddress: No header', () => { - expect(detect.getIpAddress({ headers: {} } as any)).toEqual(null); + expect(detect.getIpAddress(new Headers())).toEqual(null); }); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 7b8ac823..d67566b8 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,41 +1,83 @@ +import bcrypt from 'bcryptjs'; import { Report } from '@prisma/client'; -import { getClient } from '@umami/redis-client'; +import redis from '@/lib/redis'; import debug from 'debug'; -import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants'; -import { secret } from 'lib/crypto'; -import { NextApiRequest } from 'next'; -import { createSecureToken, ensureArray, getRandomChars, parseToken } from 'next-basics'; -import { getTeamUser, getWebsite } from 'queries'; +import { PERMISSIONS, ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants'; +import { secret, getRandomChars } from '@/lib/crypto'; +import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt'; +import { ensureArray } from '@/lib/utils'; +import { getTeamUser, getUser, getWebsite } from '@/queries'; import { Auth } from './types'; const log = debug('umami:auth'); const cloudMode = process.env.CLOUD_MODE; +const SALT_ROUNDS = 10; + +export function hashPassword(password: string, rounds = SALT_ROUNDS) { + return bcrypt.hashSync(password, rounds); +} + +export function checkPassword(password: string, passwordHash: string) { + return bcrypt.compareSync(password, passwordHash); +} + +export async function checkAuth(request: Request) { + const token = request.headers.get('authorization')?.split(' ')?.[1]; + const payload = parseSecureToken(token, secret()); + const shareToken = await parseShareToken(request.headers); + + let user = null; + const { userId, authKey, grant } = payload || {}; + + if (userId) { + user = await getUser(userId); + } else if (redis.enabled && authKey) { + const key = await redis.client.get(authKey); + + if (key?.userId) { + user = await getUser(key.userId); + } + } + + if (process.env.NODE_ENV === 'development') { + log('checkAuth:', { token, shareToken, payload, user, grant }); + } + + if (!user?.id && !shareToken) { + log('checkAuth: User not authorized'); + return null; + } + + if (user) { + user.isAdmin = user.role === ROLES.admin; + } + + return { + user, + grant, + token, + shareToken, + authKey, + }; +} export async function saveAuth(data: any, expire = 0) { const authKey = `auth:${getRandomChars(32)}`; - const redis = getClient(); + if (redis.enabled) { + await redis.client.set(authKey, data); - await redis.set(authKey, data); - - if (expire) { - await redis.expire(authKey, expire); + if (expire) { + await redis.client.expire(authKey, expire); + } } return createSecureToken({ authKey }, secret()); } -export function getAuthToken(req: NextApiRequest) { +export function parseShareToken(headers: Headers) { try { - return req.headers.authorization.split(' ')[1]; - } catch { - return null; - } -} - -export function parseShareToken(req: Request) { - try { - return parseToken(req.headers[SHARE_TOKEN_HEADER], secret()); + return parseToken(headers.get(SHARE_TOKEN_HEADER), secret()); } catch (e) { log(e); return null; diff --git a/src/lib/charts.ts b/src/lib/charts.ts index 8939b3c1..d805eefe 100644 --- a/src/lib/charts.ts +++ b/src/lib/charts.ts @@ -1,5 +1,5 @@ -import { formatDate } from 'lib/date'; -import { formatLongNumber } from 'lib/format'; +import { formatDate } from '@/lib/date'; +import { formatLongNumber } from '@/lib/format'; export function renderNumberLabels(label: string) { return +label > 1000 ? formatLongNumber(+label) : label; diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 5f0248b4..480084dc 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -1,8 +1,8 @@ import { ClickHouseClient, createClient } from '@clickhouse/client'; import { formatInTimeZone } from 'date-fns-tz'; import debug from 'debug'; -import { CLICKHOUSE } from 'lib/db'; -import { getWebsite } from 'queries/index'; +import { CLICKHOUSE } from '@/lib/db'; +import { getWebsite } from '@/queries'; import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants'; import { maxDate } from './date'; import { filtersToArray } from './params'; @@ -95,7 +95,7 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) arr.push(`and ${mapFilter(column, operator, name)}`); if (name === 'referrer') { - arr.push('and referrer_domain != {websiteDomain:String}'); + arr.push(`and referrer_domain != hostname`); } } @@ -145,7 +145,6 @@ async function parseFilters(websiteId: string, filters: QueryFilters = {}, optio ...getFilterParams(filters), websiteId, startDate: maxDate(filters.startDate, new Date(website?.resetAt)), - websiteDomain: website.domain, }, }; } @@ -157,12 +156,12 @@ async function pagedQuery( ) { const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams; const size = +pageSize || DEFAULT_PAGE_SIZE; - const offset = +size * (page - 1); + const offset = +size * (+page - 1); const direction = sortDescending ? 'desc' : 'asc'; const statements = [ orderBy && `order by ${orderBy} ${direction}`, - +size > 0 && `limit ${+size} offset ${offset}`, + +size > 0 && `limit ${+size} offset ${+offset}`, ] .filter(n => n) .join('\n'); diff --git a/src/lib/client.ts b/src/lib/client.ts index 7810c44a..795e7780 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -1,4 +1,4 @@ -import { getItem, setItem, removeItem } from 'next-basics'; +import { getItem, setItem, removeItem } from '@/lib/storage'; import { AUTH_TOKEN } from './constants'; export function getClientAuthToken() { diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a9e13c14..a64210ec 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -241,12 +241,6 @@ export const CHART_COLORS = [ export const DOMAIN_REGEX = /^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-_]{1,63}\.)(xn--)?[a-z0-9-_]+(-[a-z0-9-_]+)*\.)+(xn--)?[a-z0-9-_]{2,63})$/; export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{8,16}$/; -export const UUID_REGEX = - /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/; -export const HOSTNAME_REGEX = - /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-_]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-_]*[A-Za-z0-9])$/; -export const IP_REGEX = - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:(?:(:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/; export const DATETIME_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3}(Z|\+[0-9]{2}:[0-9]{2})?)?$/; @@ -324,6 +318,104 @@ export const BROWSERS = { yandexbrowser: 'Yandex', }; +export const IP_ADDRESS_HEADERS = [ + 'cf-connecting-ip', + 'x-client-ip', + 'x-forwarded-for', + 'do-connecting-ip', + 'fastly-client-ip', + 'true-client-ip', + 'x-real-ip', + 'x-cluster-client-ip', + 'x-forwarded', + 'forwarded', + 'x-appengine-user-ip', +]; + +export const SOCIAL_DOMAINS = [ + 'facebook.com', + 'fb.com', + 'instagram.com', + 'ig.com', + 'twitter.com', + 't.co', + 'x.com', + 'linkedin.', + 'tiktok.', + 'reddit.', + 'threads.net', + 'bsky.app', + 'news.ycombinator.com', + 'snapchat.', + 'pinterest.', +]; + +export const SEARCH_DOMAINS = [ + 'google.', + 'bing.com', + 'msn.com', + 'duckduckgo.com', + 'search.brave.com', + 'yandex.', + 'baidu.com', + 'ecosia.org', + 'chatgpt.com', + 'perplexity.ai', +]; + +export const SHOPPING_DOMAINS = [ + 'amazon.', + 'ebay.com', + 'walmart.com', + 'alibab.com', + 'aliexpress.com', + 'etsy.com', + 'bestbuy.com', + 'target.com', + 'newegg.com', +]; + +export const EMAIL_DOMAINS = [ + 'gmail.', + 'mail.yahoo.', + 'outlook.', + 'hotmail.', + 'protonmail.', + 'proton.me', +]; + +export const VIDEO_DOMAINS = ['youtube.', 'twitch.']; + +export const PAID_AD_PARAMS = [ + 'utm_source=google', + 'gclid=', + 'fbclid=', + 'msclkid=', + 'dclid=', + 'twclid=', + 'li_fat_id=', + 'epik=', + 'ttclid=', + 'scid=', +]; + +export const GROUPED_DOMAINS = [ + { name: 'Google', domain: 'google.com', match: 'google.' }, + { name: 'Facebook', domain: 'facebook.com', match: 'facebook.' }, + { name: 'Reddit', domain: 'reddit.com', match: 'reddit.' }, + { name: 'LinkedIn', domain: 'linkedin.com', match: 'linkedin.' }, + { name: 'GitHub', domain: 'github.com', match: 'github.' }, + { name: 'Hacker News', domain: 'news.ycombinator.com', match: 'news.ycombinator.com' }, + { name: 'Bing', domain: 'bing.com', match: 'bing.' }, + { name: 'Brave', domain: 'brave.com', match: 'brave.' }, + { name: 'DuckDuckGo', domain: 'duckduckgo.com', match: 'duckduckgo.' }, + { name: 'Twitter', domain: 'twitter.com', match: ['twitter.', 't.co', 'x.com'] }, + { name: 'Instagram', domain: 'instagram.com', match: ['instagram.', 'ig.com'] }, + { name: 'Snapchat', domain: 'snapchat.com', match: 'snapchat.' }, + { name: 'Pinterest', domain: 'pinterest.com', match: 'pinterest.' }, + { name: 'ChatGPT', domain: 'chatgpt.com', match: 'chatgpt.' }, +]; + export const MAP_FILE = '/datamaps.world.json'; export const ISO_COUNTRIES = { diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 689efe62..a4ff3a52 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,7 +1,78 @@ +import crypto from 'crypto'; import { startOfHour, startOfMonth } from 'date-fns'; -import { hash } from 'next-basics'; +import prand from 'pure-rand'; import { v4, v5 } from 'uuid'; +const ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH = 16; +const SALT_LENGTH = 64; +const TAG_LENGTH = 16; +const TAG_POSITION = SALT_LENGTH + IV_LENGTH; +const ENC_POSITION = TAG_POSITION + TAG_LENGTH; + +const HASH_ALGO = 'sha512'; +const HASH_ENCODING = 'hex'; + +const seed = Date.now() ^ (Math.random() * 0x100000000); +const rng = prand.xoroshiro128plus(seed); + +export function random(min: number, max: number) { + return prand.unsafeUniformIntDistribution(min, max, rng); +} + +export function getRandomChars( + n: number, + chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', +) { + const arr = chars.split(''); + let s = ''; + for (let i = 0; i < n; i++) { + s += arr[random(0, arr.length - 1)]; + } + return s; +} + +const getKey = (password: string, salt: Buffer) => + crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha512'); + +export function encrypt(value: any, secret: any) { + const iv = crypto.randomBytes(IV_LENGTH); + const salt = crypto.randomBytes(SALT_LENGTH); + const key = getKey(secret, salt); + + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + + const encrypted = Buffer.concat([cipher.update(String(value), 'utf8'), cipher.final()]); + + const tag = cipher.getAuthTag(); + + return Buffer.concat([salt, iv, tag, encrypted]).toString('base64'); +} + +export function decrypt(value: any, secret: any) { + const str = Buffer.from(String(value), 'base64'); + const salt = str.subarray(0, SALT_LENGTH); + const iv = str.subarray(SALT_LENGTH, TAG_POSITION); + const tag = str.subarray(TAG_POSITION, ENC_POSITION); + const encrypted = str.subarray(ENC_POSITION); + + const key = getKey(secret, salt); + + const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); + + decipher.setAuthTag(tag); + + return decipher.update(encrypted) + decipher.final('utf8'); +} + +export function hash(...args: string[]) { + return crypto.createHash(HASH_ALGO).update(args.join('')).digest(HASH_ENCODING); +} + +export function md5(...args: string[]) { + return crypto.createHash('md5').update(args.join('')).digest('hex'); +} + export function secret() { return hash(process.env.APP_SECRET || process.env.DATABASE_URL); } diff --git a/src/lib/date.ts b/src/lib/date.ts index b7755ffc..96135845 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -35,8 +35,8 @@ import { endOfMinute, isSameDay, } from 'date-fns'; -import { getDateLocale } from 'lib/lang'; -import { DateRange } from 'lib/types'; +import { getDateLocale } from '@/lib/lang'; +import { DateRange } from '@/lib/types'; export const TIME_UNIT = { minute: 'minute', diff --git a/src/lib/detect.ts b/src/lib/detect.ts index c3ce6fee..cd91069e 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -1,34 +1,45 @@ import path from 'path'; -import { getClientIp } from 'request-ip'; import { browserName, detectOS } from 'detect-browser'; import isLocalhost from 'is-localhost-ip'; import ipaddr from 'ipaddr.js'; import maxmind from 'maxmind'; -import { safeDecodeURIComponent } from 'next-basics'; import { DESKTOP_OS, MOBILE_OS, DESKTOP_SCREEN_WIDTH, LAPTOP_SCREEN_WIDTH, MOBILE_SCREEN_WIDTH, + IP_ADDRESS_HEADERS, } from './constants'; -import { NextApiRequestCollect } from 'pages/api/send'; -let lookup; +const MAXMIND = 'maxmind'; -export function getIpAddress(req: NextApiRequestCollect) { - const customHeader = String(process.env.CLIENT_IP_HEADER).toLowerCase(); +export function getIpAddress(headers: Headers) { + const customHeader = process.env.CLIENT_IP_HEADER; - // Custom header - if (customHeader !== 'undefined' && req.headers[customHeader]) { - return req.headers[customHeader]; - } - // Cloudflare - else if (req.headers['cf-connecting-ip']) { - return req.headers['cf-connecting-ip']; + if (customHeader && headers.get(customHeader)) { + return headers.get(customHeader); } - return getClientIp(req); + const header = IP_ADDRESS_HEADERS.find(name => { + return headers.get(name); + }); + + const ip = headers.get(header); + + if (header === 'x-forwarded-for') { + return ip?.split(',')?.[0]?.trim(); + } + + if (header === 'forwarded') { + const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/); + + if (match) { + return match[1]; + } + } + + return ip; } export function getDevice(screen: string, os: string) { @@ -67,7 +78,7 @@ function getRegionCode(country: string, region: string) { return region.includes('-') ? region : `${country}-${region}`; } -function safeDecodeCfHeader(s: string | undefined | null): string | undefined | null { +function decodeHeader(s: string | undefined | null): string | undefined | null { if (s === undefined || s === null) { return s; } @@ -75,46 +86,48 @@ function safeDecodeCfHeader(s: string | undefined | null): string | undefined | return Buffer.from(s, 'latin1').toString('utf-8'); } -export async function getLocation(ip: string, req: NextApiRequestCollect) { +export async function getLocation(ip: string = '', headers: Headers) { // Ignore local ips if (await isLocalhost(ip)) { return; } - // Cloudflare headers - if (req.headers['cf-ipcountry']) { - const country = safeDecodeCfHeader(req.headers['cf-ipcountry']); - const subdivision1 = safeDecodeCfHeader(req.headers['cf-region-code']); - const city = safeDecodeCfHeader(req.headers['cf-ipcity']); + if (!process.env.SKIP_LOCATION_HEADERS) { + // Cloudflare headers + if (headers.get('cf-ipcountry')) { + const country = decodeHeader(headers.get('cf-ipcountry')); + const subdivision1 = decodeHeader(headers.get('cf-region-code')); + const city = decodeHeader(headers.get('cf-ipcity')); - return { - country, - subdivision1: getRegionCode(country, subdivision1), - city, - }; - } + return { + country, + subdivision1: getRegionCode(country, subdivision1), + city, + }; + } - // Vercel headers - if (req.headers['x-vercel-ip-country']) { - const country = safeDecodeURIComponent(req.headers['x-vercel-ip-country']); - const subdivision1 = safeDecodeURIComponent(req.headers['x-vercel-ip-country-region']); - const city = safeDecodeURIComponent(req.headers['x-vercel-ip-city']); + // Vercel headers + if (headers.get('x-vercel-ip-country')) { + const country = decodeHeader(headers.get('x-vercel-ip-country')); + const subdivision1 = decodeHeader(headers.get('x-vercel-ip-country-region')); + const city = decodeHeader(headers.get('x-vercel-ip-city')); - return { - country, - subdivision1: getRegionCode(country, subdivision1), - city, - }; + return { + country, + subdivision1: getRegionCode(country, subdivision1), + city, + }; + } } // Database lookup - if (!lookup) { + if (!global[MAXMIND]) { const dir = path.join(process.cwd(), 'geo'); - lookup = await maxmind.open(path.resolve(dir, 'GeoLite2-City.mmdb')); + global[MAXMIND] = await maxmind.open(path.resolve(dir, 'GeoLite2-City.mmdb')); } - const result = lookup.get(ip); + const result = global[MAXMIND].get(ip); if (result) { const country = result.country?.iso_code ?? result?.registered_country?.iso_code; @@ -131,22 +144,22 @@ export async function getLocation(ip: string, req: NextApiRequestCollect) { } } -export async function getClientInfo(req: NextApiRequestCollect) { - const userAgent = req.headers['user-agent']; - const ip = req.body?.payload?.ip || getIpAddress(req); - const location = await getLocation(ip, req); - const country = location?.country; +export async function getClientInfo(request: Request, payload: Record) { + const userAgent = payload?.userAgent || request.headers.get('user-agent'); + const ip = payload?.ip || getIpAddress(request.headers); + const location = await getLocation(ip, request.headers); + const country = payload?.userAgent || location?.country; const subdivision1 = location?.subdivision1; const subdivision2 = location?.subdivision2; const city = location?.city; const browser = browserName(userAgent); const os = detectOS(userAgent) as string; - const device = getDevice(req.body?.payload?.screen, os); + const device = getDevice(payload?.screen, os); return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device }; } -export function hasBlockedIp(req: NextApiRequestCollect) { +export function hasBlockedIp(clientIp: string) { const ignoreIps = process.env.IGNORE_IP; if (ignoreIps) { @@ -156,17 +169,19 @@ export function hasBlockedIp(req: NextApiRequestCollect) { ips.push(...ignoreIps.split(',').map(n => n.trim())); } - const clientIp = getIpAddress(req); - return ips.find(ip => { - if (ip === clientIp) return true; + if (ip === clientIp) { + return true; + } // CIDR notation if (ip.indexOf('/') > 0) { const addr = ipaddr.parse(clientIp); const range = ipaddr.parseCIDR(ip); - if (addr.kind() === range[0].kind() && addr.match(range)) return true; + if (addr.kind() === range[0].kind() && addr.match(range)) { + return true; + } } }); } diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts new file mode 100644 index 00000000..ee9e160f --- /dev/null +++ b/src/lib/fetch.ts @@ -0,0 +1,51 @@ +import { buildUrl } from '@/lib/url'; + +export interface FetchResponse { + ok: boolean; + status: number; + data?: any; + error?: any; +} + +export async function request( + method: string, + url: string, + body?: string, + headers: object = {}, +): Promise { + return fetch(url, { + method, + cache: 'no-cache', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...headers, + }, + body, + }).then(async res => { + const data = await res.json(); + + return { + ok: res.ok, + status: res.status, + data: res.ok ? data : undefined, + error: res.ok ? undefined : data, + }; + }); +} + +export async function httpGet(url: string, params: object = {}, headers: object = {}) { + return request('GET', buildUrl(url, params), undefined, headers); +} + +export async function httpDelete(url: string, params: object = {}, headers: object = {}) { + return request('DELETE', buildUrl(url, params), undefined, headers); +} + +export async function httpPost(url: string, params: object = {}, headers: object = {}) { + return request('POST', url, JSON.stringify(params), headers); +} + +export async function httpPut(url: string, params: object = {}, headers: object = {}) { + return request('PUT', url, JSON.stringify(params), headers); +} diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts new file mode 100644 index 00000000..470c48ff --- /dev/null +++ b/src/lib/jwt.ts @@ -0,0 +1,36 @@ +import jwt from 'jsonwebtoken'; +import { decrypt, encrypt } from '@/lib/crypto'; + +export function createToken(payload: any, secret: any, options?: any) { + return jwt.sign(payload, secret, options); +} + +export function parseToken(token: string, secret: any) { + try { + return jwt.verify(token, secret); + } catch { + return null; + } +} + +export function createSecureToken(payload: any, secret: any, options?: any) { + return encrypt(createToken(payload, secret, options), secret); +} + +export function parseSecureToken(token: string, secret: any) { + try { + return jwt.verify(decrypt(token, secret), secret); + } catch { + return null; + } +} + +export async function parseAuthToken(req: Request, secret: string) { + try { + const token = req.headers.get('authorization')?.split(' ')?.[1]; + + return parseSecureToken(token as string, secret); + } catch { + return null; + } +} diff --git a/src/lib/kafka.ts b/src/lib/kafka.ts index 38a7073e..e7f06910 100644 --- a/src/lib/kafka.ts +++ b/src/lib/kafka.ts @@ -1,9 +1,13 @@ +import { serializeError } from 'serialize-error'; import debug from 'debug'; -import { Kafka, Mechanism, Producer, RecordMetadata, SASLOptions, logLevel } from 'kafkajs'; -import { KAFKA, KAFKA_PRODUCER } from 'lib/db'; +import { Kafka, Producer, RecordMetadata, SASLOptions, logLevel } from 'kafkajs'; +import { KAFKA, KAFKA_PRODUCER } from '@/lib/db'; import * as tls from 'tls'; const log = debug('umami:kafka'); +const CONNECT_TIMEOUT = 5000; +const SEND_TIMEOUT = 3000; +const ACKS = 1; let kafka: Kafka; let producer: Producer; @@ -12,13 +16,16 @@ const enabled = Boolean(process.env.KAFKA_URL && process.env.KAFKA_BROKER); function getClient() { const { username, password } = new URL(process.env.KAFKA_URL); const brokers = process.env.KAFKA_BROKER.split(','); + const mechanism = process.env.KAFKA_SASL_MECHANISM as 'plain' | 'scram-sha-256' | 'scram-sha-512'; - const ssl: { ssl?: tls.ConnectionOptions | boolean; sasl?: SASLOptions | Mechanism } = + const ssl: { ssl?: tls.ConnectionOptions | boolean; sasl?: SASLOptions } = username && password ? { - ssl: true, + ssl: { + rejectUnauthorized: false, + }, sasl: { - mechanism: 'scram-sha-256', + mechanism, username, password, }, @@ -28,7 +35,7 @@ function getClient() { const client: Kafka = new Kafka({ clientId: 'umami', brokers: brokers, - connectionTimeout: 3000, + connectionTimeout: CONNECT_TIMEOUT, logLevel: logLevel.ERROR, ...ssl, }); @@ -57,31 +64,29 @@ async function getProducer(): Promise { async function sendMessage( topic: string, - message: { [key: string]: string | number }, + message: { [key: string]: string | number } | { [key: string]: string | number }[], ): Promise { - await connect(); + try { + await connect(); - return producer.send({ - topic, - messages: [ - { - value: JSON.stringify(message), - }, - ], - acks: -1, - }); -} - -async function sendMessages(topic: string, messages: { [key: string]: string | number }[]) { - await connect(); - - await producer.send({ - topic, - messages: messages.map(a => { - return { value: JSON.stringify(a) }; - }), - acks: 1, - }); + return producer.send({ + topic, + messages: Array.isArray(message) + ? message.map(a => { + return { value: JSON.stringify(a) }; + }) + : [ + { + value: JSON.stringify(message), + }, + ], + timeout: SEND_TIMEOUT, + acks: ACKS, + }); + } catch (e) { + // eslint-disable-next-line no-console + console.log('KAFKA ERROR:', serializeError(e)); + } } async function connect(): Promise { @@ -103,5 +108,4 @@ export default { log, connect, sendMessage, - sendMessages, }; diff --git a/src/lib/load.ts b/src/lib/load.ts index 5b834ca8..d9aa23c2 100644 --- a/src/lib/load.ts +++ b/src/lib/load.ts @@ -1,14 +1,12 @@ -import { getWebsiteSession, getWebsite } from 'queries'; import { Website, Session } from '@prisma/client'; -import { getClient, redisEnabled } from '@umami/redis-client'; +import redis from '@/lib/redis'; +import { getWebsiteSession, getWebsite } from '@/queries'; export async function fetchWebsite(websiteId: string): Promise { let website = null; - if (redisEnabled) { - const redis = getClient(); - - website = await redis.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400); + if (redis.enabled) { + website = await redis.client.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400); } else { website = await getWebsite(websiteId); } @@ -23,10 +21,8 @@ export async function fetchWebsite(websiteId: string): Promise { export async function fetchSession(websiteId: string, sessionId: string): Promise { let session = null; - if (redisEnabled) { - const redis = getClient(); - - session = await redis.fetch( + if (redis.enabled) { + session = await redis.client.fetch( `session:${sessionId}`, () => getWebsiteSession(websiteId, sessionId), 86400, diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts deleted file mode 100644 index 3f7b9504..00000000 --- a/src/lib/middleware.ts +++ /dev/null @@ -1,105 +0,0 @@ -import cors from 'cors'; -import debug from 'debug'; -import { getClient, redisEnabled } from '@umami/redis-client'; -import { getAuthToken, parseShareToken } from 'lib/auth'; -import { ROLES } from 'lib/constants'; -import { secret } from 'lib/crypto'; -import { getSession } from 'lib/session'; -import { - badRequest, - createMiddleware, - notFound, - parseSecureToken, - unauthorized, -} from 'next-basics'; -import { NextApiRequestCollect } from 'pages/api/send'; -import { getUser } from '../queries'; - -const log = debug('umami:middleware'); - -export const useCors = createMiddleware( - cors({ - // Cache CORS preflight request 24 hours by default - maxAge: Number(process.env.CORS_MAX_AGE) || 86400, - }), -); - -export const useSession = createMiddleware(async (req, res, next) => { - try { - const session = await getSession(req as NextApiRequestCollect); - - if (!session) { - log('useSession: Session not found'); - return badRequest(res, 'Session not found.'); - } - - (req as any).session = session; - } catch (e: any) { - if (e.message.startsWith('Website not found')) { - return notFound(res, e.message); - } - return badRequest(res, e.message); - } - - next(); -}); - -export const useAuth = createMiddleware(async (req, res, next) => { - const token = getAuthToken(req); - const payload = parseSecureToken(token, secret()); - const shareToken = await parseShareToken(req as any); - - let user = null; - const { userId, authKey, grant } = payload || {}; - - if (userId) { - user = await getUser(userId); - } else if (redisEnabled && authKey) { - const redis = getClient(); - - const key = await redis.get(authKey); - - if (key?.userId) { - user = await getUser(key.userId); - } - } - - if (process.env.NODE_ENV === 'development') { - log('useAuth:', { token, shareToken, payload, user, grant }); - } - - if (!user?.id && !shareToken) { - log('useAuth: User not authorized'); - return unauthorized(res); - } - - if (user) { - user.isAdmin = user.role === ROLES.admin; - } - - (req as any).auth = { - user, - grant, - token, - shareToken, - authKey, - }; - - next(); -}); - -export const useValidate = async (schema, req, res) => { - return createMiddleware(async (req: any, res, next) => { - try { - const rules = schema[req.method]; - - if (rules) { - rules.validateSync({ ...req.query, ...req.body }); - } - } catch (e: any) { - return badRequest(res, e.message); - } - - next(); - })(req, res); -}; diff --git a/src/lib/params.ts b/src/lib/params.ts index ef4568ba..8e631ed8 100644 --- a/src/lib/params.ts +++ b/src/lib/params.ts @@ -1,5 +1,5 @@ -import { FILTER_COLUMNS, OPERATOR_PREFIXES, OPERATORS } from 'lib/constants'; -import { QueryFilters, QueryOptions } from 'lib/types'; +import { FILTER_COLUMNS, OPERATOR_PREFIXES, OPERATORS } from '@/lib/constants'; +import { QueryFilters, QueryOptions } from '@/lib/types'; export function parseParameterValue(param: any) { if (typeof param === 'string') { diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 735984db..e2f50a6c 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,8 +1,7 @@ import debug from 'debug'; -import { Prisma } from '@prisma/client'; import prisma from '@umami/prisma-client'; import { formatInTimeZone } from 'date-fns-tz'; -import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; +import { MYSQL, POSTGRESQL, getDatabaseType } from '@/lib/db'; import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants'; import { fetchWebsite } from './load'; import { maxDate } from './date'; @@ -123,7 +122,7 @@ function getSearchSQL(column: string, param: string = 'search'): string { const db = getDatabaseType(); const like = db === POSTGRESQL ? 'ilike' : 'like'; - return `and ${column} ${like} {{${param}}`; + return `and ${column} ${like} {{${param}}}`; } function mapFilter(column: string, operator: string, name: string, type: string = '') { @@ -152,7 +151,7 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): if (name === 'referrer') { arr.push( - 'and (website_event.referrer_domain != {{websiteDomain}} or website_event.referrer_domain is null)', + `and (website_event.referrer_domain != session.hostname or website_event.referrer_domain is null)`, ); } } @@ -206,7 +205,6 @@ async function parseFilters( ...getFilterParams(filters), websiteId, startDate: maxDate(filters.startDate, website?.resetAt), - websiteDomain: website.domain, }, }; } @@ -244,7 +242,7 @@ async function pagedQuery(model: string, criteria: T, pageParams: PageParams) const data = await prisma.client[model].findMany({ ...criteria, ...{ - ...(size > 0 && { take: +size, skip: +size * (page - 1) }), + ...(size > 0 && { take: +size, skip: +size * (+page - 1) }), ...(orderBy && { orderBy: [ { @@ -267,7 +265,7 @@ async function pagedRawQuery( ) { const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams; const size = +pageSize || DEFAULT_PAGE_SIZE; - const offset = +size * (page - 1); + const offset = +size * (+page - 1); const direction = sortDescending ? 'desc' : 'asc'; const statements = [ @@ -286,7 +284,7 @@ async function pagedRawQuery( return { data, count, page: +page, pageSize: size, orderBy }; } -function getQueryMode(): { mode?: Prisma.QueryMode } { +function getQueryMode(): { mode?: 'default' | 'insensitive' } { const db = getDatabaseType(); if (db === POSTGRESQL) { diff --git a/src/lib/redis.ts b/src/lib/redis.ts new file mode 100644 index 00000000..868b408a --- /dev/null +++ b/src/lib/redis.ts @@ -0,0 +1,17 @@ +import { REDIS, UmamiRedisClient } from '@umami/redis-client'; + +const enabled = !!process.env.REDIS_URL; + +function getClient() { + const client = new UmamiRedisClient(process.env.REDIS_URL); + + if (process.env.NODE_ENV !== 'production') { + global[REDIS] = client; + } + + return client; +} + +const client = global[REDIS] || getClient(); + +export default { client, enabled }; diff --git a/src/lib/request.ts b/src/lib/request.ts index 5e2be2fe..9d32f89b 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -1,10 +1,55 @@ -import { NextApiRequest } from 'next'; -import { getAllowedUnits, getMinimumUnit } from './date'; -import { getWebsiteDateRange } from '../queries'; -import { FILTER_COLUMNS } from 'lib/constants'; +import { ZodObject } from 'zod'; +import { FILTER_COLUMNS } from '@/lib/constants'; +import { badRequest, unauthorized } from '@/lib/response'; +import { getAllowedUnits, getMinimumUnit } from '@/lib/date'; +import { checkAuth } from '@/lib/auth'; +import { getWebsiteDateRange } from '@/queries'; -export async function getRequestDateRange(req: NextApiRequest) { - const { websiteId, startAt, endAt, unit } = req.query; +export async function getJsonBody(request: Request) { + try { + return await request.clone().json(); + } catch { + return undefined; + } +} + +export async function parseRequest( + request: Request, + schema?: ZodObject, + options?: { skipAuth: boolean }, +): Promise { + const url = new URL(request.url); + let query = Object.fromEntries(url.searchParams); + let body = await getJsonBody(request); + let error: () => void | undefined; + let auth = null; + + if (schema) { + const isGet = request.method === 'GET'; + const result = schema.safeParse(isGet ? query : body); + + if (!result.success) { + error = () => badRequest(result.error); + } else if (isGet) { + query = result.data; + } else { + body = result.data; + } + } + + if (!options?.skipAuth && !error) { + auth = await checkAuth(request); + + if (!auth) { + error = () => unauthorized(); + } + } + + return { url, query, body, auth, error }; +} + +export async function getRequestDateRange(query: Record) { + const { websiteId, startAt, endAt, unit } = query; // All-time if (+startAt === 0 && +endAt === 1) { @@ -31,9 +76,9 @@ export async function getRequestDateRange(req: NextApiRequest) { }; } -export function getRequestFilters(req: NextApiRequest) { +export function getRequestFilters(query: Record) { return Object.keys(FILTER_COLUMNS).reduce((obj, key) => { - const value = req.query[key]; + const value = query[key]; if (value !== undefined) { obj[key] = value; diff --git a/src/lib/response.ts b/src/lib/response.ts new file mode 100644 index 00000000..d50b453c --- /dev/null +++ b/src/lib/response.ts @@ -0,0 +1,29 @@ +import { serializeError } from 'serialize-error'; + +export function ok() { + return Response.json({ ok: true }); +} + +export function json(data: any) { + return Response.json(data); +} + +export function badRequest(error: any = 'Bad request') { + return Response.json({ error: serializeError(error) }, { status: 400 }); +} + +export function unauthorized(error: any = 'Unauthorized') { + return Response.json({ error: serializeError(error) }, { status: 401 }); +} + +export function forbidden(error: any = 'Forbidden') { + return Response.json({ error: serializeError(error) }, { status: 403 }); +} + +export function notFound(error: any = 'Not found') { + return Response.json({ error: serializeError(error) }, { status: 404 }); +} + +export function serverError(error: any = 'Server error') { + return Response.json({ error: serializeError(error) }, { status: 500 }); +} diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 5218af10..8df7be9f 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -1,13 +1,73 @@ -import * as yup from 'yup'; +import { z } from 'zod'; +import { isValidTimezone } from '@/lib/date'; +import { UNIT_TYPES } from './constants'; -export const dateRange = { - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), +export const filterParams = { + url: z.string().optional(), + referrer: z.string().optional(), + title: z.string().optional(), + query: z.string().optional(), + os: z.string().optional(), + browser: z.string().optional(), + device: z.string().optional(), + country: z.string().optional(), + region: z.string().optional(), + city: z.string().optional(), + tag: z.string().optional(), + host: z.string().optional(), + language: z.string().optional(), + event: z.string().optional(), }; -export const pageInfo = { - query: yup.string(), - page: yup.number().integer().positive(), - pageSize: yup.number().integer().positive().min(1).max(200), - orderBy: yup.string(), +export const pagingParams = { + page: z.coerce.number().int().positive().optional(), + pageSize: z.coerce.number().int().positive().optional(), + orderBy: z.string().optional(), + search: z.string().optional(), +}; + +export const timezoneParam = z.string().refine(value => isValidTimezone(value), { + message: 'Invalid timezone', +}); + +export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), { + message: 'Invalid unit', +}); + +export const roleParam = z.enum(['team-member', 'team-view-only', 'team-manager']); + +export const urlOrPathParam = z.string().refine( + value => { + try { + new URL(value, 'https://localhost'); + return true; + } catch { + return false; + } + }, + { + message: 'Invalid URL.', + }, +); + +export const reportTypeParam = z.enum([ + 'funnel', + 'insights', + 'retention', + 'utm', + 'goals', + 'journey', + 'revenue', +]); + +export const reportParms = { + websiteId: z.string().uuid(), + dateRange: z.object({ + startDate: z.coerce.date(), + endDate: z.coerce.date(), + num: z.coerce.number().optional(), + offset: z.coerce.number().optional(), + unit: z.string().optional(), + value: z.string().optional(), + }), }; diff --git a/src/lib/session.ts b/src/lib/session.ts deleted file mode 100644 index 5311e3fd..00000000 --- a/src/lib/session.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { secret, uuid, visitSalt } from 'lib/crypto'; -import { getClientInfo } from 'lib/detect'; -import { parseToken } from 'next-basics'; -import { NextApiRequestCollect } from 'pages/api/send'; -import { createSession } from 'queries'; -import clickhouse from './clickhouse'; -import { fetchSession, fetchWebsite } from './load'; -import { SessionData } from 'lib/types'; - -export async function getSession(req: NextApiRequestCollect): Promise { - const { payload } = req.body; - - if (!payload) { - throw new Error('Invalid payload.'); - } - - // Check if cache token is passed - const cacheToken = req.headers['x-umami-cache']; - - if (cacheToken) { - const result = await parseToken(cacheToken, secret()); - - // Token is valid - if (result) { - return result; - } - } - - // Verify payload - const { website: websiteId, hostname, screen, language } = payload; - - // Find website - const website = await fetchWebsite(websiteId); - - if (!website) { - throw new Error(`Website not found: ${websiteId}.`); - } - - const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } = - await getClientInfo(req); - - const sessionId = uuid(websiteId, hostname, ip, userAgent); - const visitId = uuid(sessionId, visitSalt()); - - // Clickhouse does not require session lookup - if (clickhouse.enabled) { - return { - id: sessionId, - websiteId, - visitId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city, - }; - } - - // Find session - let session = await fetchSession(websiteId, sessionId); - - // Create a session if not found - if (!session) { - try { - session = await createSession({ - id: sessionId, - websiteId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city, - }); - } catch (e: any) { - if (!e.message.toLowerCase().includes('unique constraint')) { - throw e; - } - } - } - - return { ...session, visitId }; -} diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 00000000..f08a7f7a --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,21 @@ +export function setItem(key: string, data: any, session?: boolean): void { + if (typeof window !== 'undefined' && data) { + return (session ? sessionStorage : localStorage).setItem(key, JSON.stringify(data)); + } +} + +export function getItem(key: string, session?: boolean): any { + if (typeof window !== 'undefined') { + const value = (session ? sessionStorage : localStorage).getItem(key); + + if (value !== 'undefined' && value !== null) { + return JSON.parse(value); + } + } +} + +export function removeItem(key: string, session?: boolean): void { + if (typeof window !== 'undefined') { + return (session ? sessionStorage : localStorage).removeItem(key); + } +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 615882ef..bcc05479 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,3 @@ -import { NextApiRequest } from 'next'; import { COLLECTION_TYPE, DATA_TYPE, @@ -8,7 +7,6 @@ import { REPORT_TYPES, ROLES, } from './constants'; -import * as yup from 'yup'; import { TIME_UNIT } from './date'; import { Dispatch, SetStateAction } from 'react'; @@ -25,9 +23,9 @@ export type KafkaTopic = ObjectValues; export type ReportType = ObjectValues; export interface PageParams { - query?: string; - page?: number; - pageSize?: number; + search?: string; + page?: string | number; + pageSize?: string; orderBy?: string; sortDescending?: boolean; } @@ -65,26 +63,6 @@ export interface Auth { }; } -export interface YupRequest { - GET?: yup.ObjectSchema; - POST?: yup.ObjectSchema; - PUT?: yup.ObjectSchema; - DELETE?: yup.ObjectSchema; -} - -export interface NextApiRequestQueryBody extends NextApiRequest { - auth?: Auth; - query: TQuery & { [key: string]: string | string[] }; - body: TBody; - headers: any; - yup: YupRequest; -} - -export interface NextApiRequestAuth extends NextApiRequest { - auth?: Auth; - headers: any; -} - export interface User { id: string; username: string; @@ -147,7 +125,7 @@ export interface WebsiteStats { visitors: { value: number; prev: number }; visits: { value: number; prev: number }; bounces: { value: number; prev: number }; - totalTime: { value: number; prev: number }; + totaltime: { value: number; prev: number }; } export interface DateRange { @@ -222,4 +200,6 @@ export interface SessionData { subdivision1: string; subdivision2: string; city: string; + ip?: string; + userAgent?: string; } diff --git a/src/lib/url.ts b/src/lib/url.ts new file mode 100644 index 00000000..a039d7d8 --- /dev/null +++ b/src/lib/url.ts @@ -0,0 +1,40 @@ +export function getQueryString(params: object = {}): string { + const searchParams = new URLSearchParams(); + + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) { + searchParams.append(key, value); + } + }); + + return searchParams.toString(); +} + +export function buildUrl(url: string, params: object = {}): string { + const queryString = getQueryString(params); + return `${url}${queryString && '?' + queryString}`; +} + +export function safeDecodeURI(s: string | undefined | null): string | undefined | null { + if (s === undefined || s === null) { + return s; + } + + try { + return decodeURI(s); + } catch (e) { + return s; + } +} + +export function safeDecodeURIComponent(s: string | undefined | null): string | undefined | null { + if (s === undefined || s === null) { + return s; + } + + try { + return decodeURIComponent(s); + } catch (e) { + return s; + } +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 00000000..2b0d9ff7 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,46 @@ +export function hook( + _this: { [x: string]: any }, + method: string | number, + callback: (arg0: any) => void, +) { + const orig = _this[method]; + + return (...args: any) => { + callback.apply(_this, args); + + return orig.apply(_this, args); + }; +} + +export function sleep(ms: number | undefined) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export function shuffleArray(a) { + const arr = a.slice(); + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + return arr; +} + +export function chunkArray(arr: any[], size: number) { + const chunks: any[] = []; + + let index = 0; + while (index < arr.length) { + chunks.push(arr.slice(index, size + index)); + index += size; + } + + return chunks; +} + +export function ensureArray(arr?: any) { + if (arr === undefined || arr === null) return []; + if (Array.isArray(arr)) return arr; + return [arr]; +} diff --git a/src/lib/yup.ts b/src/lib/yup.ts deleted file mode 100644 index d2652eda..00000000 --- a/src/lib/yup.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as yup from 'yup'; -import { isValidTimezone } from 'lib/date'; -import { UNIT_TYPES } from './constants'; - -export const TimezoneTest = yup - .string() - .default('UTC') - .test( - 'timezone', - () => `Invalid timezone`, - value => isValidTimezone(value), - ); - -export const UnitTypeTest = yup.string().test( - 'unit', - () => `Invalid unit`, - value => UNIT_TYPES.includes(value), -); diff --git a/src/pages/api/admin/users.ts b/src/pages/api/admin/users.ts deleted file mode 100644 index 4f03ec9f..00000000 --- a/src/pages/api/admin/users.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { canViewUsers } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUsers } from 'queries'; -import * as yup from 'yup'; - -export interface UsersRequestQuery extends PageParams {} -export interface UsersRequestBody { - userId: string; - username: string; - password: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - userId: yup.string().uuid(), - username: yup.string().max(255).required(), - password: yup.string().required(), - role: yup - .string() - .matches(/admin|user|view-only/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - if (!(await canViewUsers(req.auth))) { - return unauthorized(res); - } - - const users = await getUsers( - { - include: { - _count: { - select: { - websiteUser: { - where: { deletedAt: null }, - }, - }, - }, - }, - }, - req.query, - ); - - return ok(res, users); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/admin/websites.ts b/src/pages/api/admin/websites.ts deleted file mode 100644 index d7dd6b74..00000000 --- a/src/pages/api/admin/websites.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { canViewAllWebsites } from 'lib/auth'; -import { ROLES } from 'lib/constants'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { pageInfo } from 'lib/schema'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsites } from 'queries'; -import * as yup from 'yup'; - -export interface WebsitesRequestQuery extends PageParams { - userId?: string; - includeOwnedTeams?: boolean; - includeAllTeams?: boolean; -} - -export interface WebsitesRequestBody { - name: string; - domain: string; - shareId: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(100).required(), - domain: yup.string().max(500).required(), - shareId: yup.string().max(50).nullable(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - if (!(await canViewAllWebsites(req.auth))) { - return unauthorized(res); - } - - const { userId, includeOwnedTeams, includeAllTeams } = req.query; - - const websites = await getWebsites( - { - where: { - OR: [ - ...(userId && [{ userId }]), - ...(userId && includeOwnedTeams - ? [ - { - team: { - deletedAt: null, - teamUser: { - some: { - role: ROLES.teamOwner, - userId, - }, - }, - }, - }, - ] - : []), - ...(userId && includeAllTeams - ? [ - { - team: { - deletedAt: null, - teamUser: { - some: { - userId, - }, - }, - }, - }, - ] - : []), - ], - }, - include: { - user: { - select: { - username: true, - id: true, - }, - }, - team: { - where: { - deletedAt: null, - }, - include: { - teamUser: { - where: { - role: ROLES.teamOwner, - }, - }, - }, - }, - }, - }, - req.query, - ); - - return ok(res, websites); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts deleted file mode 100644 index ab17c937..00000000 --- a/src/pages/api/auth/login.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { redisEnabled } from '@umami/redis-client'; -import { saveAuth } from 'lib/auth'; -import { secret } from 'lib/crypto'; -import { useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { - checkPassword, - createSecureToken, - forbidden, - methodNotAllowed, - ok, - unauthorized, -} from 'next-basics'; -import { getUserByUsername } from 'queries'; -import * as yup from 'yup'; -import { ROLES } from 'lib/constants'; - -export interface LoginRequestBody { - username: string; - password: string; -} - -export interface LoginResponse { - token: string; - user: User; -} - -const schema = { - POST: yup.object().shape({ - username: yup.string().required(), - password: yup.string().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - if (process.env.DISABLE_LOGIN) { - return forbidden(res); - } - - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { username, password } = req.body; - - const user = await getUserByUsername(username, { includePassword: true }); - - if (user && checkPassword(password, user.password)) { - if (redisEnabled) { - const token = await saveAuth({ userId: user.id }); - - return ok(res, { token, user }); - } - - const token = createSecureToken({ userId: user.id }, secret()); - const { id, username, role, createdAt } = user; - - return ok(res, { - token, - user: { id, username, role, createdAt, isAdmin: role === ROLES.admin }, - }); - } - - return unauthorized(res, 'message.incorrect-username-password'); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/logout.ts b/src/pages/api/auth/logout.ts deleted file mode 100644 index f1604989..00000000 --- a/src/pages/api/auth/logout.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { methodNotAllowed, ok } from 'next-basics'; -import { getClient, redisEnabled } from '@umami/redis-client'; -import { useAuth } from 'lib/middleware'; -import { getAuthToken } from 'lib/auth'; -import { NextApiRequest, NextApiResponse } from 'next'; - -export default async (req: NextApiRequest, res: NextApiResponse) => { - await useAuth(req, res); - - if (req.method === 'POST') { - if (redisEnabled) { - const redis = getClient(); - - await redis.del(getAuthToken(req)); - } - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/auth/sso.ts b/src/pages/api/auth/sso.ts deleted file mode 100644 index c5560cb1..00000000 --- a/src/pages/api/auth/sso.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NextApiRequestAuth } from 'lib/types'; -import { useAuth } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { badRequest, ok } from 'next-basics'; -import { redisEnabled } from '@umami/redis-client'; -import { saveAuth } from 'lib/auth'; - -export default async (req: NextApiRequestAuth, res: NextApiResponse) => { - await useAuth(req, res); - - if (redisEnabled && req.auth.user) { - const token = await saveAuth({ userId: req.auth.user.id }, 86400); - - return ok(res, { user: req.auth.user, token }); - } - - return badRequest(res); -}; diff --git a/src/pages/api/auth/verify.ts b/src/pages/api/auth/verify.ts deleted file mode 100644 index 3cc78ed3..00000000 --- a/src/pages/api/auth/verify.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NextApiRequestAuth } from 'lib/types'; -import { useAuth } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { ok } from 'next-basics'; - -export default async (req: NextApiRequestAuth, res: NextApiResponse) => { - await useAuth(req, res); - - const { user } = req.auth; - - return ok(res, user); -}; diff --git a/src/pages/api/config.ts b/src/pages/api/config.ts deleted file mode 100644 index adba894a..00000000 --- a/src/pages/api/config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { ok, methodNotAllowed } from 'next-basics'; - -export interface ConfigResponse { - telemetryDisabled: boolean; - trackerScriptName: string; - uiDisabled: boolean; - updatesDisabled: boolean; -} - -export default async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method === 'GET') { - return ok(res, { - telemetryDisabled: !!process.env.DISABLE_TELEMETRY, - trackerScriptName: process.env.TRACKER_SCRIPT_NAME, - uiDisabled: !!process.env.DISABLE_UI, - updatesDisabled: !!process.env.DISABLE_UPDATES, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/heartbeat.ts b/src/pages/api/heartbeat.ts deleted file mode 100644 index 1b515d39..00000000 --- a/src/pages/api/heartbeat.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { ok } from 'next-basics'; - -export default async (req: NextApiRequest, res: NextApiResponse) => { - return ok(res); -}; diff --git a/src/pages/api/me/index.ts b/src/pages/api/me/index.ts deleted file mode 100644 index 93e97067..00000000 --- a/src/pages/api/me/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextApiResponse } from 'next'; -import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { ok } from 'next-basics'; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - - return ok(res, req.auth.user); -}; diff --git a/src/pages/api/me/password.ts b/src/pages/api/me/password.ts deleted file mode 100644 index 2ba91d86..00000000 --- a/src/pages/api/me/password.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { - badRequest, - checkPassword, - forbidden, - hashPassword, - methodNotAllowed, - ok, -} from 'next-basics'; -import { getUser, updateUser } from 'queries'; -import * as yup from 'yup'; - -export interface UserPasswordRequestBody { - currentPassword: string; - newPassword: string; -} - -const schema = { - POST: yup.object().shape({ - currentPassword: yup.string().required(), - newPassword: yup.string().min(8).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - if (process.env.CLOUD_MODE) { - return forbidden(res); - } - - await useAuth(req, res); - await useValidate(schema, req, res); - - const { currentPassword, newPassword } = req.body; - const { id: userId } = req.auth.user; - - if (req.method === 'POST') { - const user = await getUser(userId, { includePassword: true }); - - if (!checkPassword(currentPassword, user.password)) { - return badRequest(res, 'Current password is incorrect'); - } - - const password = hashPassword(newPassword); - - const updated = await updateUser(userId, { password }); - - return ok(res, updated); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/me/teams.ts b/src/pages/api/me/teams.ts deleted file mode 100644 index 3b88689d..00000000 --- a/src/pages/api/me/teams.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed } from 'next-basics'; -import userTeamsRoute from 'pages/api/users/[userId]/teams'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - req.query.userId = req.auth.user.id; - - return userTeamsRoute(req, res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/me/websites.ts b/src/pages/api/me/websites.ts deleted file mode 100644 index 48800f90..00000000 --- a/src/pages/api/me/websites.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed } from 'next-basics'; -import userWebsitesRoute from 'pages/api/users/[userId]/websites'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - req.query.userId = req.auth.user.id; - - return userWebsitesRoute(req, res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/realtime/[websiteId].ts b/src/pages/api/realtime/[websiteId].ts deleted file mode 100644 index 08e9bc47..00000000 --- a/src/pages/api/realtime/[websiteId].ts +++ /dev/null @@ -1,43 +0,0 @@ -import { startOfMinute, subMinutes } from 'date-fns'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRealtimeData } from 'queries'; -import * as yup from 'yup'; -import { REALTIME_RANGE } from 'lib/constants'; -import { TimezoneTest } from 'lib/yup'; - -export interface RealtimeRequestQuery { - websiteId: string; - timezone?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - timezone: TimezoneTest, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, timezone } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); - - const data = await getRealtimeData(websiteId, { startDate, timezone }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/[reportId].ts b/src/pages/api/reports/[reportId].ts deleted file mode 100644 index 91b5fb51..00000000 --- a/src/pages/api/reports/[reportId].ts +++ /dev/null @@ -1,102 +0,0 @@ -import { canDeleteReport, canUpdateReport, canViewReport } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, ReportType, YupRequest } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { deleteReport, getReport, updateReport } from 'queries'; -import * as yup from 'yup'; - -export interface ReportRequestQuery { - reportId: string; -} - -export interface ReportRequestBody { - websiteId: string; - type: ReportType; - name: string; - description: string; - parameters: string; -} - -const schema: YupRequest = { - GET: yup.object().shape({ - reportId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - reportId: yup.string().uuid().required(), - websiteId: yup.string().uuid().required(), - type: yup - .string() - .matches(/funnel|insights|retention|utm|goals|journey|revenue/i) - .required(), - name: yup.string().max(200).required(), - description: yup.string().max(500), - parameters: yup - .object() - .test('len', 'Must not exceed 6000 characters.', val => JSON.stringify(val).length < 6000), - }), - DELETE: yup.object().shape({ - reportId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { reportId } = req.query; - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - const report = await getReport(reportId); - - if (!(await canViewReport(req.auth, report))) { - return unauthorized(res); - } - - report.parameters = JSON.parse(report.parameters); - - return ok(res, report); - } - - if (req.method === 'POST') { - const { websiteId, type, name, description, parameters } = req.body; - - const report = await getReport(reportId); - - if (!(await canUpdateReport(req.auth, report))) { - return unauthorized(res); - } - - const result = await updateReport(reportId, { - websiteId, - userId, - type, - name, - description, - parameters: JSON.stringify(parameters), - } as any); - - return ok(res, result); - } - - if (req.method === 'DELETE') { - const report = await getReport(reportId); - - if (!(await canDeleteReport(req.auth, report))) { - return unauthorized(res); - } - - await deleteReport(reportId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/funnel.ts b/src/pages/api/reports/funnel.ts deleted file mode 100644 index f3ea41aa..00000000 --- a/src/pages/api/reports/funnel.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getFunnel } from 'queries'; -import * as yup from 'yup'; - -export interface FunnelRequestBody { - websiteId: string; - steps: { type: string; value: string }[]; - window: number; - dateRange: { - startDate: string; - endDate: string; - }; -} - -export interface FunnelResponse { - steps: { type: string; value: string }[]; - window: number; - startAt: number; - endAt: number; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - steps: yup - .array() - .of( - yup.object().shape({ - type: yup.string().required(), - value: yup - .string() - .matches(/^[a-zA-Z0-9/*-_]+$/, 'Invalid URL pattern') - .required(), - }), - ) - .min(2) - .required(), - window: yup.number().positive().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - steps, - window, - dateRange: { startDate, endDate }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getFunnel(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - steps, - windowMinutes: +window, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/goals.ts b/src/pages/api/reports/goals.ts deleted file mode 100644 index f775dc3c..00000000 --- a/src/pages/api/reports/goals.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getGoals } from 'queries/analytics/reports/getGoals'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string; timezone: string }; - goals: { type: string; value: string; goal: number }[]; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - timezone: TimezoneTest, - }) - .required(), - goals: yup - .array() - .of( - yup.object().shape({ - type: yup - .string() - .matches(/url|event|event-data/i) - .required(), - value: yup.string().required(), - goal: yup.number().required(), - operator: yup - .string() - .matches(/count|sum|average/i) - .when('type', { - is: 'eventData', - then: yup.string().required(), - }), - property: yup.string().when('type', { - is: 'eventData', - then: yup.string().required(), - }), - }), - ) - .min(1) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - goals, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getGoals(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - goals, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/index.ts b/src/pages/api/reports/index.ts deleted file mode 100644 index 38996b7a..00000000 --- a/src/pages/api/reports/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { uuid } from 'lib/crypto'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createReport, getReports } from 'queries'; -import * as yup from 'yup'; -import { canUpdateWebsite, canViewTeam, canViewWebsite } from 'lib/auth'; - -export interface ReportRequestBody { - websiteId: string; - name: string; - type: string; - description: string; - parameters: { - [key: string]: any; - }; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - name: yup.string().max(200).required(), - type: yup - .string() - .matches(/funnel|insights|retention|utm|goals|journey|revenue/i) - .required(), - description: yup.string().max(500), - parameters: yup - .object() - .test('len', 'Must not exceed 6000 characters.', val => JSON.stringify(val).length < 6000), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - const { page, query, pageSize, websiteId, teamId } = req.query; - const filters = { - page, - pageSize, - query, - }; - - if ( - (websiteId && !(await canViewWebsite(req.auth, websiteId))) || - (teamId && !(await canViewTeam(req.auth, teamId))) - ) { - return unauthorized(res); - } - - const data = await getReports( - { - where: { - OR: [ - ...(websiteId ? [{ websiteId }] : []), - ...(teamId - ? [ - { - website: { - deletedAt: null, - teamId, - }, - }, - ] - : []), - ...(userId && !websiteId && !teamId - ? [ - { - website: { - deletedAt: null, - userId, - }, - }, - ] - : []), - ], - }, - include: { - website: { - select: { - domain: true, - }, - }, - }, - }, - filters, - ); - - return ok(res, data); - } - - if (req.method === 'POST') { - const { websiteId, type, name, description, parameters } = req.body; - - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const result = await createReport({ - id: uuid(), - userId, - websiteId, - type, - name, - description, - parameters: JSON.stringify(parameters), - } as any); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/insights.ts b/src/pages/api/reports/insights.ts deleted file mode 100644 index ba4f643e..00000000 --- a/src/pages/api/reports/insights.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getInsights } from 'queries'; -import * as yup from 'yup'; - -export interface InsightsRequestBody { - websiteId: string; - dateRange: { - startDate: string; - endDate: string; - }; - fields: { name: string; type: string; label: string }[]; - filters: { name: string; type: string; operator: string; value: string }[]; - groups: { name: string; type: string }[]; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - fields: yup - .array() - .of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - label: yup.string().required(), - }), - ) - .min(1) - .required(), - filters: yup.array().of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - operator: yup.string().required(), - value: yup.string().required(), - }), - ), - groups: yup.array().of( - yup.object().shape({ - name: yup.string().required(), - type: yup.string().required(), - }), - ), - }), -}; - -function convertFilters(filters: any[]) { - return filters.reduce((obj, filter) => { - obj[filter.name] = filter; - - return obj; - }, {}); -} - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - fields, - filters, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getInsights(websiteId, fields, { - ...convertFilters(filters), - startDate: new Date(startDate), - endDate: new Date(endDate), - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/journey.ts b/src/pages/api/reports/journey.ts deleted file mode 100644 index dd3bd57b..00000000 --- a/src/pages/api/reports/journey.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getJourney } from 'queries'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string }; - steps: number; - startStep?: string; - endStep?: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - steps: yup.number().min(3).max(7).required(), - startStep: yup.string(), - endStep: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - steps, - startStep, - endStep, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getJourney(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - steps, - startStep, - endStep, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/retention.ts b/src/pages/api/reports/retention.ts deleted file mode 100644 index f4d9b7df..00000000 --- a/src/pages/api/reports/retention.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRetention } from 'queries'; -import * as yup from 'yup'; - -export interface RetentionRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string }; - timezone: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - }) - .required(), - timezone: TimezoneTest, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - timezone, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRetention(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - timezone, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/revenue.ts b/src/pages/api/reports/revenue.ts deleted file mode 100644 index d23ce55a..00000000 --- a/src/pages/api/reports/revenue.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getRevenue } from 'queries/analytics/reports/getRevenue'; -import { getRevenueValues } from 'queries/analytics/reports/getRevenueValues'; -import * as yup from 'yup'; - -export interface RevenueRequestBody { - websiteId: string; - currency?: string; - timezone?: string; - dateRange: { startDate: string; endDate: string; unit?: string }; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - timezone: TimezoneTest, - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - unit: UnitTypeTest, - }) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startDate, endDate } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRevenueValues(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - }); - - return ok(res, data); - } - - if (req.method === 'POST') { - const { - websiteId, - currency, - timezone, - dateRange: { startDate, endDate, unit }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getRevenue(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - unit, - timezone, - currency, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/reports/utm.ts b/src/pages/api/reports/utm.ts deleted file mode 100644 index 59399ee4..00000000 --- a/src/pages/api/reports/utm.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { TimezoneTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUTM } from 'queries'; -import * as yup from 'yup'; - -export interface UTMRequestBody { - websiteId: string; - dateRange: { startDate: string; endDate: string; timezone: string }; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - dateRange: yup - .object() - .shape({ - startDate: yup.date().required(), - endDate: yup.date().required(), - timezone: TimezoneTest, - }) - .required(), - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate, timezone }, - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getUTM(websiteId, { - startDate: new Date(startDate), - endDate: new Date(endDate), - timezone, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/send.ts b/src/pages/api/send.ts deleted file mode 100644 index ddaaca94..00000000 --- a/src/pages/api/send.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { isbot } from 'isbot'; -import { NextApiRequest, NextApiResponse } from 'next'; -import { - badRequest, - createToken, - forbidden, - methodNotAllowed, - ok, - safeDecodeURI, - send, -} from 'next-basics'; -import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants'; -import { secret, visitSalt, uuid } from 'lib/crypto'; -import { hasBlockedIp } from 'lib/detect'; -import { useCors, useSession, useValidate } from 'lib/middleware'; -import { CollectionType, YupRequest } from 'lib/types'; -import { saveEvent, saveSessionData } from 'queries'; -import * as yup from 'yup'; - -export interface CollectRequestBody { - payload: { - website: string; - data?: { [key: string]: any }; - hostname?: string; - ip?: string; - language?: string; - name?: string; - referrer?: string; - screen?: string; - tag?: string; - title?: string; - url: string; - }; - type: CollectionType; -} - -export interface NextApiRequestCollect extends NextApiRequest { - body: CollectRequestBody; - session: { - id: string; - websiteId: string; - visitId: string; - hostname: string; - browser: string; - os: string; - device: string; - screen: string; - language: string; - country: string; - subdivision1: string; - subdivision2: string; - city: string; - iat: number; - }; - headers: { [key: string]: any }; - yup: YupRequest; -} - -const schema = { - POST: yup.object().shape({ - payload: yup - .object() - .shape({ - data: yup.object(), - hostname: yup.string().matches(HOSTNAME_REGEX).max(100), - ip: yup.string().matches(IP_REGEX), - language: yup.string().max(35), - referrer: yup.string(), - screen: yup.string().max(11), - title: yup.string(), - url: yup.string(), - website: yup.string().uuid().required(), - name: yup.string().max(50), - tag: yup.string().max(50).nullable(), - }) - .required(), - type: yup - .string() - .matches(/event|identify/i) - .required(), - }), -}; - -export default async (req: NextApiRequestCollect, res: NextApiResponse) => { - await useCors(req, res); - - if (req.method === 'POST') { - if (!process.env.DISABLE_BOT_CHECK && isbot(req.headers['user-agent'])) { - return ok(res, { beep: 'boop' }); - } - - await useValidate(schema, req, res); - - if (hasBlockedIp(req)) { - return forbidden(res); - } - - const { type, payload } = req.body; - const { url, referrer, name: eventName, data, title, tag } = payload; - const pageTitle = safeDecodeURI(title); - - await useSession(req, res); - - const session = req.session; - - if (!session?.id) { - return; - } - - const iat = Math.floor(new Date().getTime() / 1000); - - // expire visitId after 30 minutes - if (session.iat && iat - session.iat > 1800) { - session.visitId = uuid(session.id, visitSalt()); - } - - session.iat = iat; - - if (type === COLLECTION_TYPE.event) { - // eslint-disable-next-line prefer-const - let [urlPath, urlQuery] = safeDecodeURI(url)?.split('?') || []; - let [referrerPath, referrerQuery] = safeDecodeURI(referrer)?.split('?') || []; - let referrerDomain = ''; - - if (!urlPath) { - urlPath = '/'; - } - - if (/^[\w-]+:\/\/\w+/.test(referrerPath)) { - const refUrl = new URL(referrer); - referrerPath = refUrl.pathname; - referrerQuery = refUrl.search.substring(1); - referrerDomain = refUrl.hostname.replace(/www\./, ''); - } - - if (process.env.REMOVE_TRAILING_SLASH) { - urlPath = urlPath.replace(/(.+)\/$/, '$1'); - } - - await saveEvent({ - urlPath, - urlQuery, - referrerPath, - referrerQuery, - referrerDomain, - pageTitle, - eventName, - eventData: data, - ...session, - sessionId: session.id, - tag, - }); - } else if (type === COLLECTION_TYPE.identify) { - if (!data) { - return badRequest(res, 'Data required.'); - } - - await saveSessionData({ - websiteId: session.websiteId, - sessionId: session.id, - sessionData: data, - }); - } - - const token = createToken(session, secret()); - - return send(res, token); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/share/[shareId].ts b/src/pages/api/share/[shareId].ts deleted file mode 100644 index 26ac4cdc..00000000 --- a/src/pages/api/share/[shareId].ts +++ /dev/null @@ -1,46 +0,0 @@ -import { secret } from 'lib/crypto'; -import { useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { createToken, methodNotAllowed, notFound, ok } from 'next-basics'; -import { getSharedWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface ShareRequestQuery { - shareId: string; -} - -export interface ShareResponse { - shareId: string; - token: string; -} - -const schema = { - GET: yup.object().shape({ - shareId: yup.string().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useValidate(schema, req, res); - - const { shareId } = req.query; - - if (req.method === 'GET') { - const website = await getSharedWebsite(shareId); - - if (website) { - const data = { websiteId: website.id }; - const token = createToken(data, secret()); - - return ok(res, { ...data, token }); - } - - return notFound(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/index.ts b/src/pages/api/teams/[teamId]/index.ts deleted file mode 100644 index b731ee0c..00000000 --- a/src/pages/api/teams/[teamId]/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Team } from '@prisma/client'; -import { canDeleteTeam, canUpdateTeam, canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, notFound, ok, unauthorized } from 'next-basics'; -import { deleteTeam, getTeam, updateTeam } from 'queries'; -import * as yup from 'yup'; - -export interface TeamRequestQuery { - teamId: string; -} - -export interface TeamRequestBody { - name: string; - accessCode: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - id: yup.string().uuid().required(), - name: yup.string().max(50), - accessCode: yup.string().max(50), - }), - DELETE: yup.object().shape({ - teamId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const team = await getTeam(teamId, { includeMembers: true }); - - if (!team) { - return notFound(res); - } - - return ok(res, team); - } - - if (req.method === 'POST') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const { name, accessCode } = req.body; - const data = { name, accessCode }; - - const updated = await updateTeam(teamId, data); - - return ok(res, updated); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - await deleteTeam(teamId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/users/[userId].ts b/src/pages/api/teams/[teamId]/users/[userId].ts deleted file mode 100644 index c1e80b1a..00000000 --- a/src/pages/api/teams/[teamId]/users/[userId].ts +++ /dev/null @@ -1,85 +0,0 @@ -import { canDeleteTeamUser, canUpdateTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { deleteTeamUser, getTeamUser, updateTeamUser } from 'queries'; -import * as yup from 'yup'; - -export interface TeamUserRequestQuery { - teamId: string; - userId: string; -} - -export interface TeamUserRequestBody { - role: string; -} - -const schema = { - DELETE: yup.object().shape({ - teamId: yup.string().uuid().required(), - userId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - role: yup - .string() - .matches(/team-member|team-view-only|team-manager/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId, userId } = req.query; - - if (req.method === 'GET') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - return ok(res, teamUser); - } - - if (req.method === 'POST') { - if (!(await canUpdateTeam(req.auth, teamId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - if (!teamUser) { - return badRequest(res, 'The User does not exists on this team.'); - } - - const { role } = req.body; - - await updateTeamUser(teamUser.id, { role }); - - return ok(res); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteTeamUser(req.auth, teamId, userId))) { - return unauthorized(res, 'You must be the owner of this team.'); - } - - const teamUser = await getTeamUser(teamId, userId); - - if (!teamUser) { - return badRequest(res, 'The User does not exists on this team.'); - } - - await deleteTeamUser(teamId, userId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/users/index.ts b/src/pages/api/teams/[teamId]/users/index.ts deleted file mode 100644 index f25b99da..00000000 --- a/src/pages/api/teams/[teamId]/users/index.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { canAddUserToTeam, canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { pageInfo } from 'lib/schema'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamUser, getTeamUser, getTeamUsers } from 'queries'; -import * as yup from 'yup'; - -export interface TeamUserRequestQuery extends PageParams { - teamId: string; -} - -export interface TeamUserRequestBody { - userId: string; - role: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - ...pageInfo, - }), - POST: yup.object().shape({ - userId: yup.string().uuid().required(), - role: yup - .string() - .matches(/team-member|team-view-only|team-manager/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const users = await getTeamUsers( - { - where: { - teamId, - user: { - deletedAt: null, - }, - }, - include: { - user: { - select: { - id: true, - username: true, - }, - }, - }, - }, - req.query, - ); - - return ok(res, users); - } - - // admin function only - if (req.method === 'POST') { - if (!(await canAddUserToTeam(req.auth))) { - return unauthorized(res); - } - - const { userId, role } = req.body; - - const teamUser = await getTeamUser(teamId, userId); - - if (teamUser) { - return badRequest(res, 'User is already a member of the Team.'); - } - - const users = await createTeamUser(userId, teamId, role); - - return ok(res, users); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/[teamId]/websites/index.ts b/src/pages/api/teams/[teamId]/websites/index.ts deleted file mode 100644 index 75020fa4..00000000 --- a/src/pages/api/teams/[teamId]/websites/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as yup from 'yup'; -import { canViewTeam } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { ok, unauthorized } from 'next-basics'; -import { getTeamWebsites } from 'queries'; - -export interface TeamWebsiteRequestQuery extends PageParams { - teamId: string; -} - -const schema = { - GET: yup.object().shape({ - teamId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { teamId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewTeam(req.auth, teamId))) { - return unauthorized(res); - } - - const websites = await getTeamWebsites(teamId, req.query); - - return ok(res, websites); - } -}; diff --git a/src/pages/api/teams/index.ts b/src/pages/api/teams/index.ts deleted file mode 100644 index 1e683469..00000000 --- a/src/pages/api/teams/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as yup from 'yup'; -import { Team } from '@prisma/client'; -import { canCreateTeam } from 'lib/auth'; -import { uuid } from 'lib/crypto'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeam } from 'queries'; - -export interface TeamsRequestQuery extends PageParams {} -export interface TeamsRequestBody { - name: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(50).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'POST') { - if (!(await canCreateTeam(req.auth))) { - return unauthorized(res); - } - - const { name } = req.body; - - const team = await createTeam( - { - id: uuid(), - name, - accessCode: getRandomChars(16), - }, - userId, - ); - - return ok(res, team); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/teams/join.ts b/src/pages/api/teams/join.ts deleted file mode 100644 index a9943f64..00000000 --- a/src/pages/api/teams/join.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Team } from '@prisma/client'; -import { ROLES } from 'lib/constants'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, notFound, ok } from 'next-basics'; -import { createTeamUser, findTeam, getTeamUser } from 'queries'; -import * as yup from 'yup'; - -export interface TeamsJoinRequestBody { - accessCode: string; -} - -const schema = { - POST: yup.object().shape({ - accessCode: yup.string().max(50).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - const { accessCode } = req.body; - - const team = await findTeam({ - where: { - accessCode, - }, - }); - - if (!team) { - return notFound(res, 'message.team-not-found'); - } - - const teamUser = await getTeamUser(team.id, req.auth.user.id); - - if (teamUser) { - return methodNotAllowed(res, 'message.team-already-member'); - } - - await createTeamUser(req.auth.user.id, team.id, ROLES.teamMember); - - return ok(res, team); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/index.ts b/src/pages/api/users/[userId]/index.ts deleted file mode 100644 index d69cad3c..00000000 --- a/src/pages/api/users/[userId]/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import * as yup from 'yup'; -import { canDeleteUser, canUpdateUser, canViewUser } from 'lib/auth'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, User } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { deleteUser, getUser, getUserByUsername, updateUser } from 'queries'; - -export interface UserRequestQuery { - userId: string; -} - -export interface UserRequestBody { - userId: string; - username: string; - password: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - userId: yup.string().uuid().required(), - username: yup.string().max(255), - password: yup.string(), - role: yup.string().matches(/admin|user|view-only/i), - }), - DELETE: yup.object().shape({ - userId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { isAdmin }, - } = req.auth; - const userId: string = req.query.userId; - - if (req.method === 'GET') { - if (!(await canViewUser(req.auth, userId))) { - return unauthorized(res); - } - - const user = await getUser(userId); - - return ok(res, user); - } - - if (req.method === 'POST') { - if (!(await canUpdateUser(req.auth, userId))) { - return unauthorized(res); - } - - const { username, password, role } = req.body; - - const user = await getUser(userId); - - const data: any = {}; - - if (password) { - data.password = hashPassword(password); - } - - // Only admin can change these fields - if (role && isAdmin) { - data.role = role; - } - - if (username && isAdmin) { - data.username = username; - } - - // Check when username changes - if (data.username && user.username !== data.username) { - const user = await getUserByUsername(username); - - if (user) { - return badRequest(res, 'User already exists'); - } - } - - const updated = await updateUser(userId, data); - - return ok(res, updated); - } - - if (req.method === 'DELETE') { - if (!(await canDeleteUser(req.auth))) { - return unauthorized(res); - } - - if (userId === req.auth.user.id) { - return badRequest(res, 'You cannot delete yourself.'); - } - - await deleteUser(userId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/teams.ts b/src/pages/api/users/[userId]/teams.ts deleted file mode 100644 index 3f2af9e2..00000000 --- a/src/pages/api/users/[userId]/teams.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as yup from 'yup'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserTeams } from 'queries'; - -export interface UserTeamsRequestQuery extends PageParams { - userId: string; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - const { userId } = req.query; - - if (req.method === 'GET') { - if (!user.isAdmin && (!userId || user.id !== userId)) { - return unauthorized(res); - } - - const teams = await getUserTeams(userId as string, req.query); - - return ok(res, teams); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/usage.ts b/src/pages/api/users/[userId]/usage.ts deleted file mode 100644 index b5000395..00000000 --- a/src/pages/api/users/[userId]/usage.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getAllUserWebsitesIncludingTeamOwner, getEventDataUsage, getEventUsage } from 'queries'; -import * as yup from 'yup'; - -export interface UserUsageRequestQuery { - userId: string; - startAt: string; - endAt: string; -} - -export interface UserUsageRequestResponse { - websiteEventUsage: number; - eventDataUsage: number; - websites: { - websiteEventUsage: number; - eventDataUsage: number; - websiteId: string; - websiteName: string; - }[]; -} - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - - if (req.method === 'GET') { - if (!user.isAdmin) { - return unauthorized(res); - } - - const { userId, startAt, endAt } = req.query; - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const websites = await getAllUserWebsitesIncludingTeamOwner(userId); - - const websiteIds = websites.map(a => a.id); - - const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate); - const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate); - - const websiteUsage = websites.map(a => ({ - websiteId: a.id, - websiteName: a.name, - websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0, - eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0, - deletedAt: a.deletedAt, - })); - - const usage = websiteUsage.reduce( - (acc, cv) => { - acc.websiteEventUsage += cv.websiteEventUsage; - acc.eventDataUsage += cv.eventDataUsage; - - return acc; - }, - { websiteEventUsage: 0, eventDataUsage: 0 }, - ); - - const filteredWebsiteUsage = websiteUsage.filter( - a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0), - ); - - return ok(res, { - ...usage, - websites: filteredWebsiteUsage, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/[userId]/websites.ts b/src/pages/api/users/[userId]/websites.ts deleted file mode 100644 index 88a2bad1..00000000 --- a/src/pages/api/users/[userId]/websites.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserWebsites } from 'queries'; -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - userId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { user } = req.auth; - const { userId, page = 1, pageSize, query = '', ...rest } = req.query; - - if (req.method === 'GET') { - if (!user.isAdmin && user.id !== userId) { - return unauthorized(res); - } - - const websites = await getUserWebsites(userId, { - page, - pageSize, - query, - ...rest, - }); - - return ok(res, websites); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/users/index.ts b/src/pages/api/users/index.ts deleted file mode 100644 index 333670a9..00000000 --- a/src/pages/api/users/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { canCreateUser } from 'lib/auth'; -import { ROLES } from 'lib/constants'; -import { uuid } from 'lib/crypto'; -import { useAuth, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, Role, PageParams, User } from 'lib/types'; -import { pageInfo } from 'lib/schema'; -import { NextApiResponse } from 'next'; -import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createUser, getUserByUsername } from 'queries'; -import * as yup from 'yup'; - -export interface UsersRequestQuery extends PageParams {} -export interface UsersRequestBody { - username: string; - password: string; - id: string; - role: Role; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - username: yup.string().max(255).required(), - password: yup.string().required(), - id: yup.string().uuid(), - role: yup - .string() - .matches(/admin|user|view-only/i) - .required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'POST') { - if (!(await canCreateUser(req.auth))) { - return unauthorized(res); - } - - const { username, password, role, id } = req.body; - - const existingUser = await getUserByUsername(username, { showDeleted: true }); - - if (existingUser) { - return badRequest(res, 'User already exists'); - } - - const created = await createUser({ - id: id || uuid(), - username, - password: hashPassword(password), - role: role ?? ROLES.user, - }); - - return ok(res, created); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/version.ts b/src/pages/api/version.ts deleted file mode 100644 index 4453b56f..00000000 --- a/src/pages/api/version.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { ok, methodNotAllowed } from 'next-basics'; -import { CURRENT_VERSION } from 'lib/constants'; - -export interface VersionResponse { - version: string; -} - -export default async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method === 'GET') { - return ok(res, { - version: CURRENT_VERSION, - }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/active.ts b/src/pages/api/websites/[websiteId]/active.ts deleted file mode 100644 index d87a7818..00000000 --- a/src/pages/api/websites/[websiteId]/active.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getActiveVisitors } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteActiveRequestQuery { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId as string))) { - return unauthorized(res); - } - - const result = await getActiveVisitors(websiteId as string); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/daterange.ts b/src/pages/api/websites/[websiteId]/daterange.ts deleted file mode 100644 index 1aeb76cb..00000000 --- a/src/pages/api/websites/[websiteId]/daterange.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { WebsiteActive, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteDateRange } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteDateRangeRequestQuery { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const result = await getWebsiteDateRange(websiteId); - - return ok(res, result); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/events.ts b/src/pages/api/websites/[websiteId]/event-data/events.ts deleted file mode 100644 index bf0f409a..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/events.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataEvents } from 'queries'; -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - event?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - event: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, event } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataEvents(websiteId, { - startDate, - endDate, - event, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/fields.ts b/src/pages/api/websites/[websiteId]/event-data/fields.ts deleted file mode 100644 index c5075c5e..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/fields.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataFields } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataFields(websiteId, { - startDate, - endDate, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/properties.ts b/src/pages/api/websites/[websiteId]/event-data/properties.ts deleted file mode 100644 index 19e9bbb8..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/properties.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataProperties } from 'queries'; -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataProperties(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/stats.ts b/src/pages/api/websites/[websiteId]/event-data/stats.ts deleted file mode 100644 index 7e440b88..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/stats.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataStats } from 'queries/index'; -import * as yup from 'yup'; - -export interface EventDataStatsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataStats(websiteId, { startDate, endDate }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/event-data/values.ts b/src/pages/api/websites/[websiteId]/event-data/values.ts deleted file mode 100644 index e5bb4ab8..00000000 --- a/src/pages/api/websites/[websiteId]/event-data/values.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventDataValues } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - eventName?: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - eventName: yup.string(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, eventName, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getEventDataValues(websiteId, { - startDate, - endDate, - eventName, - propertyName, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/events/index.ts b/src/pages/api/websites/[websiteId]/events/index.ts deleted file mode 100644 index 13b31fc0..00000000 --- a/src/pages/api/websites/[websiteId]/events/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteEvents } from 'queries'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteEvents(websiteId, { startDate, endDate }, req.query); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/events/series.ts b/src/pages/api/websites/[websiteId]/events/series.ts deleted file mode 100644 index 6d67a264..00000000 --- a/src/pages/api/websites/[websiteId]/events/series.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestDateRange, getRequestFilters } from 'lib/request'; -import { NextApiRequestQueryBody, WebsiteMetric } from 'lib/types'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventMetrics } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteEventsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - unit?: string; - timezone?: string; - url: string; - referrer?: string; - title?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region: string; - city?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - unit: UnitTypeTest, - timezone: TimezoneTest, - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, timezone } = req.query; - const { startDate, endDate, unit } = await getRequestDateRange(req); - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - timezone, - unit, - }; - - const events = await getEventMetrics(websiteId, filters); - - return ok(res, events); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/index.ts b/src/pages/api/websites/[websiteId]/index.ts deleted file mode 100644 index c60a8399..00000000 --- a/src/pages/api/websites/[websiteId]/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; -import { Website, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite, canUpdateWebsite, canDeleteWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; -import { SHARE_ID_REGEX } from 'lib/constants'; - -export interface WebsiteRequestQuery { - websiteId: string; -} - -export interface WebsiteRequestBody { - name: string; - domain: string; - shareId: string; -} - -import * as yup from 'yup'; - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - name: yup.string(), - domain: yup.string(), - shareId: yup.string().matches(SHARE_ID_REGEX, { excludeEmptyString: true }).nullable(), - }), - DELETE: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const website = await getWebsite(websiteId); - - return ok(res, website); - } - - if (req.method === 'POST') { - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { name, domain, shareId } = req.body; - - try { - const website = await updateWebsite(websiteId, { name, domain, shareId }); - - return ok(res, website); - } catch (e: any) { - if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { - return serverError(res, 'That share ID is already taken.'); - } - - return serverError(res, e); - } - } - - if (req.method === 'DELETE') { - if (!(await canDeleteWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - await deleteWebsite(websiteId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/metrics.ts b/src/pages/api/websites/[websiteId]/metrics.ts deleted file mode 100644 index 1996a61a..00000000 --- a/src/pages/api/websites/[websiteId]/metrics.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { WebsiteMetric, NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS, OPERATORS } from 'lib/constants'; -import { getPageviewMetrics, getSessionMetrics } from 'queries'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import * as yup from 'yup'; - -export interface WebsiteMetricsRequestQuery { - websiteId: string; - type: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - language?: string; - event?: string; - limit?: number; - offset?: number; - search?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - type: yup.string().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - language: yup.string(), - event: yup.string(), - limit: yup.number(), - offset: yup.number(), - search: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, type, limit, offset, search } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - const column = FILTER_COLUMNS[type] || type; - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - }; - - if (search) { - filters[type] = { - name: type, - column, - operator: OPERATORS.contains, - value: search, - }; - } - - if (SESSION_COLUMNS.includes(type)) { - const data = await getSessionMetrics(websiteId, type, filters, limit, offset); - - if (type === 'language') { - const combined = {}; - - for (const { x, y } of data) { - const key = String(x).toLowerCase().split('-')[0]; - - if (combined[key] === undefined) { - combined[key] = { x: key, y }; - } else { - combined[key].y += y; - } - } - - return ok(res, Object.values(combined)); - } - - return ok(res, data); - } - - if (EVENT_COLUMNS.includes(type)) { - const data = await getPageviewMetrics(websiteId, type, filters, limit, offset); - - return ok(res, data); - } - - return badRequest(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/pageviews.ts b/src/pages/api/websites/[websiteId]/pageviews.ts deleted file mode 100644 index c3b6b797..00000000 --- a/src/pages/api/websites/[websiteId]/pageviews.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getPageviewStats, getSessionStats } from 'queries'; -import { TimezoneTest, UnitTypeTest } from 'lib/yup'; -import { getCompareDate } from 'lib/date'; - -export interface WebsitePageviewRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - unit?: string; - timezone?: string; - url?: string; - referrer?: string; - title?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region: string; - city?: string; - tag?: string; - compare?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - unit: UnitTypeTest, - timezone: TimezoneTest, - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - compare: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, timezone, compare } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate, unit } = await getRequestDateRange(req); - - const filters = { - ...getRequestFilters(req), - startDate, - endDate, - timezone, - unit, - }; - - const [pageviews, sessions] = await Promise.all([ - getPageviewStats(websiteId, filters), - getSessionStats(websiteId, filters), - ]); - - if (compare) { - const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( - compare, - startDate, - endDate, - ); - - const [comparePageviews, compareSessions] = await Promise.all([ - getPageviewStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }), - getSessionStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }), - ]); - - return ok(res, { - pageviews, - sessions, - startDate, - endDate, - compare: { - pageviews: comparePageviews, - sessions: compareSessions, - startDate: compareStartDate, - endDate: compareEndDate, - }, - }); - } - - return ok(res, { pageviews, sessions }); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/reports.ts b/src/pages/api/websites/[websiteId]/reports.ts deleted file mode 100644 index 72e5b0f2..00000000 --- a/src/pages/api/websites/[websiteId]/reports.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteReports } from 'queries'; -import { pageInfo } from 'lib/schema'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { page, query, pageSize } = req.query; - - const data = await getWebsiteReports(websiteId, { - page, - pageSize, - query, - }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/reset.ts b/src/pages/api/websites/[websiteId]/reset.ts deleted file mode 100644 index 82e769dc..00000000 --- a/src/pages/api/websites/[websiteId]/reset.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canUpdateWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { resetWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteResetRequestQuery { - websiteId: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'POST') { - if (!(await canUpdateWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - await resetWebsite(websiteId); - - return ok(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/session-data/properties.ts b/src/pages/api/websites/[websiteId]/session-data/properties.ts deleted file mode 100644 index 92e182d2..00000000 --- a/src/pages/api/websites/[websiteId]/session-data/properties.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionDataProperties } from 'queries'; -import * as yup from 'yup'; - -export interface SessionDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionDataProperties(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/session-data/values.ts b/src/pages/api/websites/[websiteId]/session-data/values.ts deleted file mode 100644 index 98463f15..00000000 --- a/src/pages/api/websites/[websiteId]/session-data/values.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionDataValues } from 'queries'; - -import * as yup from 'yup'; - -export interface EventDataFieldsRequestQuery { - websiteId: string; - startAt: string; - endAt: string; - propertyName?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - propertyName: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt, propertyName } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionDataValues(websiteId, { startDate, endDate, propertyName }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts deleted file mode 100644 index 2b0fc084..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/activity.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionActivity } from 'queries'; - -export interface SessionActivityRequestQuery extends PageParams { - websiteId: string; - sessionId: string; - startAt: number; - endAt: number; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - sessionId: yup.string().uuid().required(), - startAt: yup.number().integer(), - endAt: yup.number().integer(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, sessionId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getSessionActivity(websiteId, sessionId, startDate, endDate); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/index.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/index.ts deleted file mode 100644 index f627a208..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteSession } from 'queries'; - -export interface WesiteSessionRequestQuery extends PageParams { - websiteId: string; - sessionId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - sessionId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, sessionId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getWebsiteSession(websiteId, sessionId); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/properties.ts b/src/pages/api/websites/[websiteId]/sessions/[sessionId]/properties.ts deleted file mode 100644 index c0c20064..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/[sessionId]/properties.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getSessionData } from 'queries'; -import * as yup from 'yup'; - -export interface SessionDataRequestQuery { - sessionId: string; - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - sessionId: yup.string().uuid().required(), - websiteId: yup.string().uuid().required(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - if (req.method === 'GET') { - const { websiteId, sessionId } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getSessionData(websiteId, sessionId); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/index.ts b/src/pages/api/websites/[websiteId]/sessions/index.ts deleted file mode 100644 index 1809929c..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteSessions } from 'queries'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteSessions(websiteId, { startDate, endDate }, req.query); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/stats.ts b/src/pages/api/websites/[websiteId]/sessions/stats.ts deleted file mode 100644 index fe92ce6f..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/stats.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { getRequestDateRange, getRequestFilters } from 'lib/request'; -import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getWebsiteSessionStats } from 'queries/analytics/sessions/getWebsiteSessionStats'; -import * as yup from 'yup'; - -export interface WebsiteSessionStatsRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - event?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - tag?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - event: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - - const filters = getRequestFilters(req); - - const metrics = await getWebsiteSessionStats(websiteId, { - ...filters, - startDate, - endDate, - }); - - const stats = Object.keys(metrics[0]).reduce((obj, key) => { - obj[key] = { - value: Number(metrics[0][key]) || 0, - }; - return obj; - }, {}); - - return ok(res, stats); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/sessions/weekly.ts b/src/pages/api/websites/[websiteId]/sessions/weekly.ts deleted file mode 100644 index b1c28c3f..00000000 --- a/src/pages/api/websites/[websiteId]/sessions/weekly.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as yup from 'yup'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { pageInfo } from 'lib/schema'; -import { getWebsiteSessionsWeekly } from 'queries'; -import { TimezoneTest } from 'lib/yup'; - -export interface ReportsRequestQuery extends PageParams { - websiteId: string; - timezone?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().integer().required(), - endAt: yup.number().integer().min(yup.ref('startAt')).required(), - timezone: TimezoneTest, - ...pageInfo, - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, startAt, endAt, timezone } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate, timezone }); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/stats.ts b/src/pages/api/websites/[websiteId]/stats.ts deleted file mode 100644 index dfc9198d..00000000 --- a/src/pages/api/websites/[websiteId]/stats.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as yup from 'yup'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types'; -import { getRequestFilters, getRequestDateRange } from 'lib/request'; -import { getWebsiteStats } from 'queries'; -import { getCompareDate } from 'lib/date'; - -export interface WebsiteStatsRequestQuery { - websiteId: string; - startAt: number; - endAt: number; - url?: string; - referrer?: string; - title?: string; - query?: string; - event?: string; - host?: string; - os?: string; - browser?: string; - device?: string; - country?: string; - region?: string; - city?: string; - tag?: string; - compare?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - url: yup.string(), - referrer: yup.string(), - title: yup.string(), - query: yup.string(), - event: yup.string(), - host: yup.string(), - os: yup.string(), - browser: yup.string(), - device: yup.string(), - country: yup.string(), - region: yup.string(), - city: yup.string(), - tag: yup.string(), - compare: yup.string(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, compare } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startDate, endDate } = await getRequestDateRange(req); - const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( - compare, - startDate, - endDate, - ); - - const filters = getRequestFilters(req); - - const metrics = await getWebsiteStats(websiteId, { - ...filters, - startDate, - endDate, - }); - - const prevPeriod = await getWebsiteStats(websiteId, { - ...filters, - startDate: compareStartDate, - endDate: compareEndDate, - }); - - const stats = Object.keys(metrics[0]).reduce((obj, key) => { - obj[key] = { - value: Number(metrics[0][key]) || 0, - prev: Number(prevPeriod[0][key]) || 0, - }; - return obj; - }, {}); - - return ok(res, stats); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/transfer.ts b/src/pages/api/websites/[websiteId]/transfer.ts deleted file mode 100644 index 56cf6bac..00000000 --- a/src/pages/api/websites/[websiteId]/transfer.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { updateWebsite } from 'queries'; -import * as yup from 'yup'; - -export interface WebsiteTransferRequestQuery { - websiteId: string; -} - -export interface WebsiteTransferRequestBody { - userId?: string; - teamId?: string; -} - -const schema = { - POST: yup.object().shape({ - websiteId: yup.string().uuid().required(), - userId: yup.string().uuid(), - teamId: yup.string().uuid(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId } = req.query; - const { userId, teamId } = req.body; - - if (req.method === 'POST') { - if (userId) { - if (!(await canTransferWebsiteToUser(req.auth, websiteId, userId))) { - return unauthorized(res); - } - - const website = await updateWebsite(websiteId, { - userId, - teamId: null, - }); - - return ok(res, website); - } else if (teamId) { - if (!(await canTransferWebsiteToTeam(req.auth, websiteId, teamId))) { - return unauthorized(res); - } - - const website = await updateWebsite(websiteId, { - userId: null, - teamId, - }); - - return ok(res, website); - } - - return badRequest(res); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/[websiteId]/values.ts b/src/pages/api/websites/[websiteId]/values.ts deleted file mode 100644 index 53d717a5..00000000 --- a/src/pages/api/websites/[websiteId]/values.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { NextApiRequestQueryBody } from 'lib/types'; -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { - badRequest, - methodNotAllowed, - ok, - safeDecodeURIComponent, - unauthorized, -} from 'next-basics'; -import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; -import { getValues } from 'queries'; -import { getRequestDateRange } from 'lib/request'; -import * as yup from 'yup'; - -export interface ValuesRequestQuery { - websiteId: string; - type: string; - startAt: number; - endAt: number; - search?: string; -} - -const schema = { - GET: yup.object().shape({ - websiteId: yup.string().uuid().required(), - type: yup.string().required(), - startAt: yup.number().required(), - endAt: yup.number().required(), - search: yup.string(), - }), -}; - -export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { websiteId, type, search } = req.query; - const { startDate, endDate } = await getRequestDateRange(req); - - if (req.method === 'GET') { - if (!SESSION_COLUMNS.includes(type as string) && !EVENT_COLUMNS.includes(type as string)) { - return badRequest(res); - } - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search); - - return ok( - res, - values - .map(({ value }) => safeDecodeURIComponent(value)) - .filter(n => n) - .sort(), - ); - } - - return methodNotAllowed(res); -}; diff --git a/src/pages/api/websites/index.ts b/src/pages/api/websites/index.ts deleted file mode 100644 index c5eb7200..00000000 --- a/src/pages/api/websites/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { canCreateTeamWebsite, canCreateWebsite } from 'lib/auth'; -import { uuid } from 'lib/crypto'; -import { useAuth, useCors, useValidate } from 'lib/middleware'; -import { NextApiRequestQueryBody, PageParams } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createWebsite } from 'queries'; -import userWebsitesRoute from 'pages/api/users/[userId]/websites'; -import * as yup from 'yup'; -import { pageInfo } from 'lib/schema'; - -export interface WebsitesRequestQuery extends PageParams {} - -export interface WebsitesRequestBody { - name: string; - domain: string; - shareId: string; - teamId: string; -} - -const schema = { - GET: yup.object().shape({ - ...pageInfo, - }), - POST: yup.object().shape({ - name: yup.string().max(100).required(), - domain: yup.string().max(500).required(), - shareId: yup.string().max(50).nullable(), - teamId: yup.string().nullable(), - }), -}; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - await useValidate(schema, req, res); - - const { - user: { id: userId }, - } = req.auth; - - if (req.method === 'GET') { - if (!req.query.userId) { - req.query.userId = userId; - } - - return userWebsitesRoute(req, res); - } - - if (req.method === 'POST') { - const { name, domain, shareId, teamId } = req.body; - - if ( - (teamId && !(await canCreateTeamWebsite(req.auth, teamId))) || - !(await canCreateWebsite(req.auth)) - ) { - return unauthorized(res); - } - - const data: any = { - id: uuid(), - createdBy: userId, - name, - domain, - shareId, - teamId, - }; - - if (!teamId) { - data.userId = userId; - } - - const website = await createWebsite(data); - - return ok(res, website); - } - - return methodNotAllowed(res); -}; diff --git a/src/queries/index.ts b/src/queries/index.ts index 8c7e564a..2f785528 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -1,39 +1,41 @@ -export * from 'queries/prisma/report'; -export * from 'queries/prisma/team'; -export * from 'queries/prisma/teamUser'; -export * from 'queries/prisma/user'; -export * from 'queries/prisma/website'; -export * from './analytics/events/getEventDataEvents'; -export * from './analytics/events/getEventDataFields'; -export * from './analytics/events/getEventDataProperties'; -export * from './analytics/events/getEventDataValues'; -export * from './analytics/events/getEventDataStats'; -export * from './analytics/events/getEventDataUsage'; -export * from './analytics/events/getEventMetrics'; -export * from './analytics/events/getWebsiteEvents'; -export * from './analytics/events/getEventUsage'; -export * from './analytics/events/saveEvent'; -export * from './analytics/reports/getFunnel'; -export * from './analytics/reports/getJourney'; -export * from './analytics/reports/getRetention'; -export * from './analytics/reports/getInsights'; -export * from './analytics/reports/getUTM'; -export * from './analytics/pageviews/getPageviewMetrics'; -export * from './analytics/pageviews/getPageviewStats'; -export * from './analytics/sessions/createSession'; -export * from './analytics/sessions/getWebsiteSession'; -export * from './analytics/sessions/getSessionData'; -export * from './analytics/sessions/getSessionDataProperties'; -export * from './analytics/sessions/getSessionDataValues'; -export * from './analytics/sessions/getSessionMetrics'; -export * from './analytics/sessions/getWebsiteSessions'; -export * from './analytics/sessions/getWebsiteSessionsWeekly'; -export * from './analytics/sessions/getSessionActivity'; -export * from './analytics/sessions/getSessionStats'; -export * from './analytics/sessions/saveSessionData'; -export * from './analytics/getActiveVisitors'; -export * from './analytics/getRealtimeActivity'; -export * from './analytics/getRealtimeData'; -export * from './analytics/getValues'; -export * from './analytics/getWebsiteDateRange'; -export * from './analytics/getWebsiteStats'; +export * from '@/queries/prisma/report'; +export * from '@/queries/prisma/team'; +export * from '@/queries/prisma/teamUser'; +export * from '@/queries/prisma/user'; +export * from '@/queries/prisma/website'; +export * from '@/queries/sql/events/getEventDataEvents'; +export * from '@/queries/sql/events/getEventDataFields'; +export * from '@/queries/sql/events/getEventDataProperties'; +export * from '@/queries/sql/events/getEventDataValues'; +export * from '@/queries/sql/events/getEventDataStats'; +export * from '@/queries/sql/events/getEventDataUsage'; +export * from '@/queries/sql/events/getEventMetrics'; +export * from '@/queries/sql/events/getWebsiteEvents'; +export * from '@/queries/sql/events/getEventUsage'; +export * from '@/queries/sql/events/saveEvent'; +export * from '@/queries/sql/reports/getFunnel'; +export * from '@/queries/sql/reports/getJourney'; +export * from '@/queries/sql/reports/getRetention'; +export * from '@/queries/sql/reports/getInsights'; +export * from '@/queries/sql/reports/getUTM'; +export * from '@/queries/sql/pageviews/getPageviewMetrics'; +export * from '@/queries/sql/pageviews/getPageviewStats'; +export * from '@/queries/sql/sessions/createSession'; +export * from '@/queries/sql/sessions/getWebsiteSession'; +export * from '@/queries/sql/sessions/getSessionData'; +export * from '@/queries/sql/sessions/getSessionDataProperties'; +export * from '@/queries/sql/sessions/getSessionDataValues'; +export * from '@/queries/sql/sessions/getSessionMetrics'; +export * from '@/queries/sql/sessions/getWebsiteSessions'; +export * from '@/queries/sql/sessions/getWebsiteSessionStats'; +export * from '@/queries/sql/sessions/getWebsiteSessionsWeekly'; +export * from '@/queries/sql/sessions/getSessionActivity'; +export * from '@/queries/sql/sessions/getSessionStats'; +export * from '@/queries/sql/sessions/saveSessionData'; +export * from '@/queries/sql/getActiveVisitors'; +export * from '@/queries/sql/getChannelMetrics'; +export * from '@/queries/sql/getRealtimeActivity'; +export * from '@/queries/sql/getRealtimeData'; +export * from '@/queries/sql/getValues'; +export * from '@/queries/sql/getWebsiteDateRange'; +export * from '@/queries/sql/getWebsiteStats'; diff --git a/src/queries/prisma/report.ts b/src/queries/prisma/report.ts index a0e6364c..4feb9fb8 100644 --- a/src/queries/prisma/report.ts +++ b/src/queries/prisma/report.ts @@ -1,6 +1,6 @@ import { Prisma, Report } from '@prisma/client'; -import prisma from 'lib/prisma'; -import { PageResult, PageParams } from 'lib/types'; +import prisma from '@/lib/prisma'; +import { PageResult, PageParams } from '@/lib/types'; import ReportFindManyArgs = Prisma.ReportFindManyArgs; async function findReport(criteria: Prisma.ReportFindUniqueArgs): Promise { @@ -19,11 +19,11 @@ export async function getReports( criteria: ReportFindManyArgs, pageParams: PageParams = {}, ): Promise> { - const { query } = pageParams; + const { search } = pageParams; const where: Prisma.ReportWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(query, [ + ...prisma.getSearchParameters(search, [ { name: 'contains' }, { description: 'contains' }, { type: 'contains' }, diff --git a/src/queries/prisma/team.ts b/src/queries/prisma/team.ts index e516c446..9862fff2 100644 --- a/src/queries/prisma/team.ts +++ b/src/queries/prisma/team.ts @@ -1,8 +1,8 @@ import { Prisma, Team } from '@prisma/client'; -import { ROLES } from 'lib/constants'; -import { uuid } from 'lib/crypto'; -import prisma from 'lib/prisma'; -import { PageResult, PageParams } from 'lib/types'; +import { ROLES } from '@/lib/constants'; +import { uuid } from '@/lib/crypto'; +import prisma from '@/lib/prisma'; +import { PageResult, PageParams } from '@/lib/types'; import TeamFindManyArgs = Prisma.TeamFindManyArgs; export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise { @@ -25,11 +25,11 @@ export async function getTeams( filters: PageParams = {}, ): Promise> { const { getSearchParameters } = prisma; - const { query } = filters; + const { search } = filters; const where: Prisma.TeamWhereInput = { ...criteria.where, - ...getSearchParameters(query, [{ name: 'contains' }]), + ...getSearchParameters(search, [{ name: 'contains' }]), }; return prisma.pagedQuery( diff --git a/src/queries/prisma/teamUser.ts b/src/queries/prisma/teamUser.ts index d172dd5a..1478ae5b 100644 --- a/src/queries/prisma/teamUser.ts +++ b/src/queries/prisma/teamUser.ts @@ -1,7 +1,7 @@ import { Prisma, TeamUser } from '@prisma/client'; -import { uuid } from 'lib/crypto'; -import prisma from 'lib/prisma'; -import { PageResult, PageParams } from 'lib/types'; +import { uuid } from '@/lib/crypto'; +import prisma from '@/lib/prisma'; +import { PageResult, PageParams } from '@/lib/types'; import TeamUserFindManyArgs = Prisma.TeamUserFindManyArgs; export async function findTeamUser(criteria: Prisma.TeamUserFindUniqueArgs): Promise { @@ -21,11 +21,11 @@ export async function getTeamUsers( criteria: TeamUserFindManyArgs, filters?: PageParams, ): Promise> { - const { query } = filters; + const { search } = filters; const where: Prisma.TeamUserWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(query, [{ user: { username: 'contains' } }]), + ...prisma.getSearchParameters(search, [{ user: { username: 'contains' } }]), }; return prisma.pagedQuery( diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts index 0c8e3520..2e6a478f 100644 --- a/src/queries/prisma/user.ts +++ b/src/queries/prisma/user.ts @@ -1,8 +1,8 @@ import { Prisma } from '@prisma/client'; -import { ROLES } from 'lib/constants'; -import prisma from 'lib/prisma'; -import { PageResult, Role, User, PageParams } from 'lib/types'; -import { getRandomChars } from 'next-basics'; +import { ROLES } from '@/lib/constants'; +import prisma from '@/lib/prisma'; +import { PageResult, Role, User, PageParams } from '@/lib/types'; +import { getRandomChars } from '@/lib/crypto'; import UserFindManyArgs = Prisma.UserFindManyArgs; export interface GetUserOptions { @@ -51,11 +51,11 @@ export async function getUsers( criteria: UserFindManyArgs, pageParams?: PageParams, ): Promise> { - const { query } = pageParams; + const { search } = pageParams; const where: Prisma.UserWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(query, [{ username: 'contains' }]), + ...prisma.getSearchParameters(search, [{ username: 'contains' }]), deletedAt: null, }; diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index dc1ec438..25328914 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -1,9 +1,9 @@ import { Prisma, Website } from '@prisma/client'; -import { getClient } from '@umami/redis-client'; -import prisma from 'lib/prisma'; -import { PageResult, PageParams } from 'lib/types'; +import redis from '@/lib/redis'; +import prisma from '@/lib/prisma'; +import { PageResult, PageParams } from '@/lib/types'; import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs; -import { ROLES } from 'lib/constants'; +import { ROLES } from '@/lib/constants'; async function findWebsite(criteria: Prisma.WebsiteFindUniqueArgs): Promise { return prisma.client.website.findUnique(criteria); @@ -30,11 +30,11 @@ export async function getWebsites( criteria: WebsiteFindManyArgs, pageParams: PageParams, ): Promise> { - const { query } = pageParams; + const { search } = pageParams; const where: Prisma.WebsiteWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(query, [ + ...prisma.getSearchParameters(search, [ { name: 'contains', }, @@ -182,9 +182,7 @@ export async function resetWebsite( }), ]).then(async data => { if (cloudMode) { - const redis = getClient(); - - await redis.set(`website:${websiteId}`, data[3]); + await redis.client.set(`website:${websiteId}`, data[3]); } return data; @@ -227,9 +225,7 @@ export async function deleteWebsite( }), ]).then(async data => { if (cloudMode) { - const redis = getClient(); - - await redis.del(`website:${websiteId}`); + await redis.client.del(`website:${websiteId}`); } return data; diff --git a/src/queries/analytics/events/getEventDataEvents.ts b/src/queries/sql/events/getEventDataEvents.ts similarity index 93% rename from src/queries/analytics/events/getEventDataEvents.ts rename to src/queries/sql/events/getEventDataEvents.ts index 0b19c5be..432c93a2 100644 --- a/src/queries/analytics/events/getEventDataEvents.ts +++ b/src/queries/sql/events/getEventDataEvents.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getEventDataEvents( ...args: [websiteId: string, filters: QueryFilters] diff --git a/src/queries/analytics/events/getEventDataFields.ts b/src/queries/sql/events/getEventDataFields.ts similarity index 90% rename from src/queries/analytics/events/getEventDataFields.ts rename to src/queries/sql/events/getEventDataFields.ts index 05fee072..33b4e0f5 100644 --- a/src/queries/analytics/events/getEventDataFields.ts +++ b/src/queries/sql/events/getEventDataFields.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getEventDataFields( ...args: [websiteId: string, filters: QueryFilters] diff --git a/src/queries/analytics/events/getEventDataProperties.ts b/src/queries/sql/events/getEventDataProperties.ts similarity index 90% rename from src/queries/analytics/events/getEventDataProperties.ts rename to src/queries/sql/events/getEventDataProperties.ts index e2cf0828..73fb8fec 100644 --- a/src/queries/analytics/events/getEventDataProperties.ts +++ b/src/queries/sql/events/getEventDataProperties.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getEventDataProperties( ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }] diff --git a/src/queries/analytics/events/getEventDataStats.ts b/src/queries/sql/events/getEventDataStats.ts similarity index 90% rename from src/queries/analytics/events/getEventDataStats.ts rename to src/queries/sql/events/getEventDataStats.ts index adeeda46..98347960 100644 --- a/src/queries/analytics/events/getEventDataStats.ts +++ b/src/queries/sql/events/getEventDataStats.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters } from '@/lib/types'; export async function getEventDataStats( ...args: [websiteId: string, filters: QueryFilters] diff --git a/src/queries/analytics/events/getEventDataUsage.ts b/src/queries/sql/events/getEventDataUsage.ts similarity index 86% rename from src/queries/analytics/events/getEventDataUsage.ts rename to src/queries/sql/events/getEventDataUsage.ts index 1d146c9c..1f2bf833 100644 --- a/src/queries/analytics/events/getEventDataUsage.ts +++ b/src/queries/sql/events/getEventDataUsage.ts @@ -1,5 +1,5 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from 'lib/db'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from '@/lib/db'; export function getEventDataUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) { return runQuery({ diff --git a/src/queries/analytics/events/getEventDataValues.ts b/src/queries/sql/events/getEventDataValues.ts similarity index 91% rename from src/queries/analytics/events/getEventDataValues.ts rename to src/queries/sql/events/getEventDataValues.ts index 63101824..c8d63362 100644 --- a/src/queries/analytics/events/getEventDataValues.ts +++ b/src/queries/sql/events/getEventDataValues.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getEventDataValues( ...args: [ diff --git a/src/queries/analytics/events/getEventMetrics.ts b/src/queries/sql/events/getEventMetrics.ts similarity index 85% rename from src/queries/analytics/events/getEventMetrics.ts rename to src/queries/sql/events/getEventMetrics.ts index 504cea11..c961c55b 100644 --- a/src/queries/analytics/events/getEventMetrics.ts +++ b/src/queries/sql/events/getEventMetrics.ts @@ -1,8 +1,8 @@ -import clickhouse from 'lib/clickhouse'; -import { EVENT_TYPE } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters, WebsiteEventMetric } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters, WebsiteEventMetric } from '@/lib/types'; export async function getEventMetrics( ...args: [websiteId: string, filters: QueryFilters] @@ -86,9 +86,5 @@ async function clickhouseQuery( `; } - return rawQuery(sql, params).then(a => { - return Object.values(a).map(a => { - return { x: a.x, t: a.t, y: Number(a.y) }; - }); - }); + return rawQuery(sql, params); } diff --git a/src/queries/analytics/events/getEventUsage.ts b/src/queries/sql/events/getEventUsage.ts similarity index 74% rename from src/queries/analytics/events/getEventUsage.ts rename to src/queries/sql/events/getEventUsage.ts index 8baefe06..9ff69a07 100644 --- a/src/queries/analytics/events/getEventUsage.ts +++ b/src/queries/sql/events/getEventUsage.ts @@ -1,5 +1,5 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from 'lib/db'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from '@/lib/db'; export function getEventUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) { return runQuery({ @@ -30,9 +30,5 @@ function clickhouseQuery( startDate, endDate, }, - ).then(a => { - return Object.values(a).map(a => { - return { websiteId: a.websiteId, count: Number(a.count) }; - }); - }); + ); } diff --git a/src/queries/analytics/events/getWebsiteEvents.ts b/src/queries/sql/events/getWebsiteEvents.ts similarity index 78% rename from src/queries/analytics/events/getWebsiteEvents.ts rename to src/queries/sql/events/getWebsiteEvents.ts index 21e6270c..6fe7a0a1 100644 --- a/src/queries/analytics/events/getWebsiteEvents.ts +++ b/src/queries/sql/events/getWebsiteEvents.ts @@ -1,7 +1,7 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { PageParams, QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { PageParams, QueryFilters } from '@/lib/types'; export function getWebsiteEvents( ...args: [websiteId: string, filters: QueryFilters, pageParams?: PageParams] @@ -14,7 +14,7 @@ export function getWebsiteEvents( async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) { const { pagedRawQuery, parseFilters } = prisma; - const { query } = pageParams; + const { search } = pageParams; const { filterQuery, params } = await parseFilters(websiteId, { ...filters, }); @@ -43,16 +43,16 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar and created_at between {{startDate}} and {{endDate}} ${filterQuery} ${ - query - ? `and ((event_name ${like} {{query}} and event_type = 2) - or (url_path ${like} {{query}} and event_type = 1))` + search + ? `and ((event_name ${like} {{search}} and event_type = 2) + or (url_path ${like} {{search}} and event_type = 1))` : '' } order by created_at desc limit 1000) select * from events `, - { ...params, query: `%${query}%` }, + { ...params, query: `%${search}%` }, pageParams, ); } @@ -60,7 +60,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) { const { pagedQuery, parseFilters } = clickhouse; const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters); - const { query } = pageParams; + const { search } = pageParams; return pagedQuery( ` @@ -83,16 +83,16 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar ${dateQuery} ${filterQuery} ${ - query - ? `and ((positionCaseInsensitive(event_name, {query:String}) > 0 and event_type = 2) - or (positionCaseInsensitive(url_path, {query:String}) > 0 and event_type = 1))` + search + ? `and ((positionCaseInsensitive(event_name, {search:String}) > 0 and event_type = 2) + or (positionCaseInsensitive(url_path, {search:String}) > 0 and event_type = 1))` : '' } order by created_at desc limit 1000) select * from events `, - { ...params, query }, + { ...params, search }, pageParams, ); } diff --git a/src/queries/analytics/events/saveEvent.ts b/src/queries/sql/events/saveEvent.ts similarity index 94% rename from src/queries/analytics/events/saveEvent.ts rename to src/queries/sql/events/saveEvent.ts index 2424186a..65ee1175 100644 --- a/src/queries/analytics/events/saveEvent.ts +++ b/src/queries/sql/events/saveEvent.ts @@ -1,9 +1,9 @@ -import { EVENT_NAME_LENGTH, URL_LENGTH, EVENT_TYPE, PAGE_TITLE_LENGTH } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import clickhouse from 'lib/clickhouse'; -import kafka from 'lib/kafka'; -import prisma from 'lib/prisma'; -import { uuid } from 'lib/crypto'; +import { EVENT_NAME_LENGTH, URL_LENGTH, EVENT_TYPE, PAGE_TITLE_LENGTH } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import clickhouse from '@/lib/clickhouse'; +import kafka from '@/lib/kafka'; +import prisma from '@/lib/prisma'; +import { uuid } from '@/lib/crypto'; import { saveEventData } from './saveEventData'; export async function saveEvent(args: { diff --git a/src/queries/analytics/events/saveEventData.ts b/src/queries/sql/events/saveEventData.ts similarity index 82% rename from src/queries/analytics/events/saveEventData.ts rename to src/queries/sql/events/saveEventData.ts index 7e7db8b4..7c158da4 100644 --- a/src/queries/analytics/events/saveEventData.ts +++ b/src/queries/sql/events/saveEventData.ts @@ -1,12 +1,12 @@ import { Prisma } from '@prisma/client'; -import { DATA_TYPE } from 'lib/constants'; -import { uuid } from 'lib/crypto'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { flattenJSON, getStringValue } from 'lib/data'; -import clickhouse from 'lib/clickhouse'; -import kafka from 'lib/kafka'; -import prisma from 'lib/prisma'; -import { DynamicData } from 'lib/types'; +import { DATA_TYPE } from '@/lib/constants'; +import { uuid } from '@/lib/crypto'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { flattenJSON, getStringValue } from '@/lib/data'; +import clickhouse from '@/lib/clickhouse'; +import kafka from '@/lib/kafka'; +import prisma from '@/lib/prisma'; +import { DynamicData } from '@/lib/types'; export async function saveEventData(data: { websiteId: string; @@ -61,7 +61,7 @@ async function clickhouseQuery(data: { const { websiteId, sessionId, eventId, urlPath, eventName, eventData, createdAt } = data; const { insert, getUTCString } = clickhouse; - const { sendMessages } = kafka; + const { sendMessage } = kafka; const jsonKeys = flattenJSON(eventData); @@ -82,7 +82,7 @@ async function clickhouseQuery(data: { }); if (kafka.enabled) { - await sendMessages('event_data', messages); + await sendMessage('event_data', messages); } else { await insert('event_data', messages); } diff --git a/src/queries/analytics/getActiveVisitors.ts b/src/queries/sql/getActiveVisitors.ts similarity index 80% rename from src/queries/analytics/getActiveVisitors.ts rename to src/queries/sql/getActiveVisitors.ts index c59a265a..e0225f3a 100644 --- a/src/queries/analytics/getActiveVisitors.ts +++ b/src/queries/sql/getActiveVisitors.ts @@ -1,7 +1,7 @@ import { subMinutes } from 'date-fns'; -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db'; export async function getActiveVisitors(...args: [websiteId: string]) { return runQuery({ @@ -15,7 +15,7 @@ async function relationalQuery(websiteId: string) { const result = await rawQuery( ` - select count(distinct session_id) x + select count(distinct session_id) as visitors from website_event where website_id = {{websiteId::uuid}} and created_at >= {{startDate}} @@ -32,7 +32,7 @@ async function clickhouseQuery(websiteId: string): Promise<{ x: number }> { const result = await rawQuery( ` select - count(distinct session_id) x + count(distinct session_id) as "visitors" from website_event where website_id = {websiteId:UUID} and created_at >= {startDate:DateTime64} diff --git a/src/queries/sql/getChannelMetrics.ts b/src/queries/sql/getChannelMetrics.ts new file mode 100644 index 00000000..9141290e --- /dev/null +++ b/src/queries/sql/getChannelMetrics.ts @@ -0,0 +1,55 @@ +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db'; +import { QueryFilters } from '@/lib/types'; + +export async function getChannelMetrics(...args: [websiteId: string, filters?: QueryFilters]) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websiteId: string, filters: QueryFilters) { + const { rawQuery, parseFilters } = prisma; + const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters); + + return rawQuery( + ` + select + referrer_domain as domain, + referrer_query as query, + count(distinct session_id) as visitors + from website_event + where website_id = {{websiteId::uuid}} + ${filterQuery} + ${dateQuery} + group by 1, 2 + order by visitors desc + `, + params, + ); +} + +async function clickhouseQuery( + websiteId: string, + filters: QueryFilters, +): Promise<{ x: string; y: number }[]> { + const { rawQuery, parseFilters } = clickhouse; + const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters); + + const sql = ` + select + referrer_domain as domain, + referrer_query as query, + uniq(session_id) as visitors + from website_event + where website_id = {websiteId:UUID} + ${filterQuery} + ${dateQuery} + group by 1, 2 + order by visitors desc + `; + + return rawQuery(sql, params); +} diff --git a/src/queries/analytics/getRealtimeActivity.ts b/src/queries/sql/getRealtimeActivity.ts similarity index 88% rename from src/queries/analytics/getRealtimeActivity.ts rename to src/queries/sql/getRealtimeActivity.ts index fba303b2..10828885 100644 --- a/src/queries/analytics/getRealtimeActivity.ts +++ b/src/queries/sql/getRealtimeActivity.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import { QueryFilters } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db'; +import { QueryFilters } from '@/lib/types'; export async function getRealtimeActivity(...args: [websiteId: string, filters: QueryFilters]) { return runQuery({ @@ -32,7 +32,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { where website_event.website_id = {{websiteId::uuid}} ${filterQuery} ${dateQuery} - order by website_event.created_at asc + order by website_event.created_at desc limit 100 `, params, diff --git a/src/queries/analytics/getRealtimeData.ts b/src/queries/sql/getRealtimeData.ts similarity index 98% rename from src/queries/analytics/getRealtimeData.ts rename to src/queries/sql/getRealtimeData.ts index 1af63219..a9b06814 100644 --- a/src/queries/analytics/getRealtimeData.ts +++ b/src/queries/sql/getRealtimeData.ts @@ -1,4 +1,4 @@ -import { getPageviewStats, getRealtimeActivity, getSessionStats } from 'queries/index'; +import { getPageviewStats, getRealtimeActivity, getSessionStats } from '@/queries'; function increment(data: object, key: string) { if (key) { diff --git a/src/queries/analytics/getValues.ts b/src/queries/sql/getValues.ts similarity index 91% rename from src/queries/analytics/getValues.ts rename to src/queries/sql/getValues.ts index f303faff..2d1286f3 100644 --- a/src/queries/analytics/getValues.ts +++ b/src/queries/sql/getValues.ts @@ -1,6 +1,6 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db'; export async function getValues( ...args: [websiteId: string, column: string, startDate: Date, endDate: Date, search: string] @@ -42,7 +42,7 @@ async function relationalQuery( return rawQuery( ` - select ${column} as "value", count(*) + select ${column} as "value", count(*) as "count" from website_event inner join session on session.session_id = website_event.session_id @@ -98,7 +98,7 @@ async function clickhouseQuery( return rawQuery( ` - select ${column} as value, count(*) + select ${column} as "value", count(*) as "count" from website_event where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} diff --git a/src/queries/analytics/getWebsiteDateRange.ts b/src/queries/sql/getWebsiteDateRange.ts similarity index 86% rename from src/queries/analytics/getWebsiteDateRange.ts rename to src/queries/sql/getWebsiteDateRange.ts index ef07712e..953fa5eb 100644 --- a/src/queries/analytics/getWebsiteDateRange.ts +++ b/src/queries/sql/getWebsiteDateRange.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import { DEFAULT_RESET_DATE } from 'lib/constants'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db'; +import { DEFAULT_RESET_DATE } from '@/lib/constants'; export async function getWebsiteDateRange(...args: [websiteId: string]) { return runQuery({ diff --git a/src/queries/analytics/getWebsiteStats.ts b/src/queries/sql/getWebsiteStats.ts similarity index 92% rename from src/queries/analytics/getWebsiteStats.ts rename to src/queries/sql/getWebsiteStats.ts index 061d487e..80f1d578 100644 --- a/src/queries/analytics/getWebsiteStats.ts +++ b/src/queries/sql/getWebsiteStats.ts @@ -1,9 +1,9 @@ -import clickhouse from 'lib/clickhouse'; -import { EVENT_TYPE } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters } from 'lib/types'; -import { EVENT_COLUMNS } from 'lib/constants'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; +import { EVENT_COLUMNS } from '@/lib/constants'; export async function getWebsiteStats( ...args: [websiteId: string, filters: QueryFilters] diff --git a/src/queries/analytics/pageviews/getPageviewMetrics.ts b/src/queries/sql/pageviews/getPageviewMetrics.ts similarity index 77% rename from src/queries/analytics/pageviews/getPageviewMetrics.ts rename to src/queries/sql/pageviews/getPageviewMetrics.ts index f734b1dd..19a9b467 100644 --- a/src/queries/analytics/pageviews/getPageviewMetrics.ts +++ b/src/queries/sql/pageviews/getPageviewMetrics.ts @@ -1,11 +1,17 @@ -import clickhouse from 'lib/clickhouse'; -import { EVENT_COLUMNS, EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_COLUMNS, EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; export async function getPageviewMetrics( - ...args: [websiteId: string, type: string, filters: QueryFilters, limit?: number, offset?: number] + ...args: [ + websiteId: string, + type: string, + filters: QueryFilters, + limit?: number | string, + offset?: number | string, + ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -17,8 +23,8 @@ async function relationalQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ) { const column = FILTER_COLUMNS[type] || type; const { rawQuery, parseFilters } = prisma; @@ -28,14 +34,15 @@ async function relationalQuery( ...filters, eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, }, - { joinSession: SESSION_COLUMNS.includes(type) }, + { joinSession: SESSION_COLUMNS.includes(type) || column === 'referrer_domain' }, ); let entryExitQuery = ''; let excludeDomain = ''; + if (column === 'referrer_domain') { - excludeDomain = `and website_event.referrer_domain != {{websiteDomain}} - and website_event.referrer_domain is not null`; + excludeDomain = `and website_event.referrer_domain != session.hostname + and website_event.referrer_domain != ''`; } if (type === 'entry' || type === 'exit') { @@ -58,7 +65,8 @@ async function relationalQuery( return rawQuery( ` - select ${column} x, count(*) y + select ${column} x, + ${column === 'referrer_domain' ? 'count(distinct website_event.session_id)' : 'count(*)'} as y from website_event ${joinSession} ${entryExitQuery} @@ -80,8 +88,8 @@ async function clickhouseQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ): Promise<{ x: string; y: number }[]> { const column = FILTER_COLUMNS[type] || type; const { rawQuery, parseFilters } = clickhouse; @@ -90,14 +98,14 @@ async function clickhouseQuery( eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, }); - let excludeDomain = ''; let sql = ''; + let excludeDomain = ''; if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) { let entryExitQuery = ''; if (column === 'referrer_domain') { - excludeDomain = `and referrer_domain != {websiteDomain:String} and referrer_domain != ''`; + excludeDomain = `and referrer_domain != hostname and referrer_domain != ''`; } if (type === 'entry' || type === 'exit') { @@ -116,7 +124,8 @@ async function clickhouseQuery( } sql = ` - select ${column} x, count(*) y + select ${column} x, + ${column === 'referrer_domain' ? 'uniq(session_id)' : 'count(*)'} as y from website_event ${entryExitQuery} where website_id = {websiteId:UUID} @@ -131,13 +140,13 @@ async function clickhouseQuery( `; } else { let groupByQuery = ''; + let columnQuery = `arrayJoin(${column})`; if (column === 'referrer_domain') { - excludeDomain = `and t != {websiteDomain:String} and t != ''`; + excludeDomain = `and t != hostname and t != ''`; + columnQuery = `session_id s, arrayJoin(${column})`; } - let columnQuery = `arrayJoin(${column})`; - if (type === 'entry') { columnQuery = `visit_id x, argMinMerge(entry_url)`; } @@ -152,7 +161,7 @@ async function clickhouseQuery( sql = ` select g.t as x, - count(*) as y + ${column === 'referrer_domain' ? 'uniq(s)' : 'count(*)'} as y from ( select ${columnQuery} as t from website_event_stats_hourly website_event @@ -169,9 +178,5 @@ async function clickhouseQuery( `; } - return rawQuery(sql, params).then((result: any) => { - return Object.values(result).map((a: any) => { - return { x: a.x, y: Number(a.y) }; - }); - }); + return rawQuery(sql, params); } diff --git a/src/queries/analytics/pageviews/getPageviewStats.ts b/src/queries/sql/pageviews/getPageviewStats.ts similarity index 90% rename from src/queries/analytics/pageviews/getPageviewStats.ts rename to src/queries/sql/pageviews/getPageviewStats.ts index 48b82000..f5ace52c 100644 --- a/src/queries/analytics/pageviews/getPageviewStats.ts +++ b/src/queries/sql/pageviews/getPageviewStats.ts @@ -1,8 +1,8 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { EVENT_COLUMNS, EVENT_TYPE } from 'lib/constants'; -import { QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { EVENT_COLUMNS, EVENT_TYPE } from '@/lib/constants'; +import { QueryFilters } from '@/lib/types'; export async function getPageviewStats(...args: [websiteId: string, filters: QueryFilters]) { return runQuery({ diff --git a/src/queries/analytics/reports/getFunnel.ts b/src/queries/sql/reports/getFunnel.ts similarity index 98% rename from src/queries/analytics/reports/getFunnel.ts rename to src/queries/sql/reports/getFunnel.ts index 3a81157f..70b51a9d 100644 --- a/src/queries/analytics/reports/getFunnel.ts +++ b/src/queries/sql/reports/getFunnel.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; const formatResults = (steps: { type: string; value: string }[]) => (results: unknown) => { return steps.map((step: { type: string; value: string }, i: number) => { diff --git a/src/queries/analytics/reports/getGoals.ts b/src/queries/sql/reports/getGoals.ts similarity index 98% rename from src/queries/analytics/reports/getGoals.ts rename to src/queries/sql/reports/getGoals.ts index 2bb29d8e..eda76050 100644 --- a/src/queries/analytics/reports/getGoals.ts +++ b/src/queries/sql/reports/getGoals.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; export async function getGoals( ...args: [ diff --git a/src/queries/analytics/reports/getInsights.ts b/src/queries/sql/reports/getInsights.ts similarity index 85% rename from src/queries/analytics/reports/getInsights.ts rename to src/queries/sql/reports/getInsights.ts index 8e6e3289..d7cdc283 100644 --- a/src/queries/analytics/reports/getInsights.ts +++ b/src/queries/sql/reports/getInsights.ts @@ -1,8 +1,8 @@ -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; -import { QueryFilters } from 'lib/types'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; +import { QueryFilters } from '@/lib/types'; export async function getInsights( ...args: [websiteId: string, fields: { name: string; type?: string }[], filters: QueryFilters] @@ -115,18 +115,7 @@ async function clickhouseQuery( limit 500 `, params, - ).then(a => { - return Object.values(a).map(a => { - return { - ...a, - views: Number(a.views), - visitors: Number(a.visitors), - visits: Number(a.visits), - bounces: Number(a.bounces), - totaltime: Number(a.totaltime), - }; - }); - }); + ); } function parseFields(fields: { name: any }[]) { diff --git a/src/queries/analytics/reports/getJourney.ts b/src/queries/sql/reports/getJourney.ts similarity index 97% rename from src/queries/analytics/reports/getJourney.ts rename to src/queries/sql/reports/getJourney.ts index eec500aa..4c43cc03 100644 --- a/src/queries/analytics/reports/getJourney.ts +++ b/src/queries/sql/reports/getJourney.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; interface JourneyResult { e1: string; diff --git a/src/queries/analytics/reports/getRetention.ts b/src/queries/sql/reports/getRetention.ts similarity index 96% rename from src/queries/analytics/reports/getRetention.ts rename to src/queries/sql/reports/getRetention.ts index d69a77d7..23854b60 100644 --- a/src/queries/analytics/reports/getRetention.ts +++ b/src/queries/sql/reports/getRetention.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; export async function getRetention( ...args: [ diff --git a/src/queries/analytics/reports/getRevenue.ts b/src/queries/sql/reports/getRevenue.ts similarity index 98% rename from src/queries/analytics/reports/getRevenue.ts rename to src/queries/sql/reports/getRevenue.ts index a2803e85..c9c7b74a 100644 --- a/src/queries/analytics/reports/getRevenue.ts +++ b/src/queries/sql/reports/getRevenue.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; export async function getRevenue( ...args: [ diff --git a/src/queries/analytics/reports/getRevenueValues.ts b/src/queries/sql/reports/getRevenueValues.ts similarity index 93% rename from src/queries/analytics/reports/getRevenueValues.ts rename to src/queries/sql/reports/getRevenueValues.ts index 4dcc4a22..a46bf0bf 100644 --- a/src/queries/analytics/reports/getRevenueValues.ts +++ b/src/queries/sql/reports/getRevenueValues.ts @@ -1,6 +1,6 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA, getDatabaseType, POSTGRESQL } from 'lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA, getDatabaseType, POSTGRESQL } from '@/lib/db'; export async function getRevenueValues( ...args: [ diff --git a/src/queries/analytics/reports/getUTM.ts b/src/queries/sql/reports/getUTM.ts similarity index 89% rename from src/queries/analytics/reports/getUTM.ts rename to src/queries/sql/reports/getUTM.ts index 4e1af9f0..5463815b 100644 --- a/src/queries/analytics/reports/getUTM.ts +++ b/src/queries/sql/reports/getUTM.ts @@ -1,7 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { safeDecodeURIComponent } from 'next-basics'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; export async function getUTM( ...args: [ @@ -84,7 +83,7 @@ function parseParameters(data: any[]) { for (const [key, value] of searchParams) { if (key.match(/^utm_(\w+)$/)) { - const name = safeDecodeURIComponent(value); + const name = value; if (!obj[key]) { obj[key] = { [name]: Number(num) }; } else if (!obj[key][name]) { diff --git a/src/queries/analytics/sessions/createSession.ts b/src/queries/sql/sessions/createSession.ts similarity index 93% rename from src/queries/analytics/sessions/createSession.ts rename to src/queries/sql/sessions/createSession.ts index 7d614499..7605cffc 100644 --- a/src/queries/analytics/sessions/createSession.ts +++ b/src/queries/sql/sessions/createSession.ts @@ -1,5 +1,5 @@ import { Prisma } from '@prisma/client'; -import prisma from 'lib/prisma'; +import prisma from '@/lib/prisma'; export async function createSession(data: Prisma.SessionCreateInput) { const { diff --git a/src/queries/analytics/sessions/getSessionActivity.ts b/src/queries/sql/sessions/getSessionActivity.ts similarity index 90% rename from src/queries/analytics/sessions/getSessionActivity.ts rename to src/queries/sql/sessions/getSessionActivity.ts index 1fe8bbd3..d7e2a413 100644 --- a/src/queries/analytics/sessions/getSessionActivity.ts +++ b/src/queries/sql/sessions/getSessionActivity.ts @@ -1,6 +1,6 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; export async function getSessionActivity( ...args: [websiteId: string, sessionId: string, startDate: Date, endDate: Date] diff --git a/src/queries/analytics/sessions/getSessionData.ts b/src/queries/sql/sessions/getSessionData.ts similarity index 91% rename from src/queries/analytics/sessions/getSessionData.ts rename to src/queries/sql/sessions/getSessionData.ts index ce80b035..a3f1e113 100644 --- a/src/queries/analytics/sessions/getSessionData.ts +++ b/src/queries/sql/sessions/getSessionData.ts @@ -1,6 +1,6 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db'; export async function getSessionData(...args: [websiteId: string, sessionId: string]) { return runQuery({ diff --git a/src/queries/analytics/sessions/getSessionDataProperties.ts b/src/queries/sql/sessions/getSessionDataProperties.ts similarity index 58% rename from src/queries/analytics/sessions/getSessionDataProperties.ts rename to src/queries/sql/sessions/getSessionDataProperties.ts index 1d15ea8d..20fb11d5 100644 --- a/src/queries/analytics/sessions/getSessionDataProperties.ts +++ b/src/queries/sql/sessions/getSessionDataProperties.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getSessionDataProperties( ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }] @@ -24,13 +24,15 @@ async function relationalQuery( return rawQuery( ` select - data_key as "propertyName", - count(*) as "total" - from session_data - where website_id = {{websiteId::uuid}} - and created_at between {{startDate}} and {{endDate}} - ${filterQuery} - group by data_key + data_key as "propertyName", + count(distinct d.session_id) as "total" + from website_event e + join session_data d + on d.session_id = e.session_id + where e.website_id = {{websiteId::uuid}} + and e.created_at between {{startDate}} and {{endDate}} + ${filterQuery} + group by 1 order by 2 desc limit 500 `, @@ -51,12 +53,15 @@ async function clickhouseQuery( ` select data_key as propertyName, - count(*) as total - from session_data final - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} + count(distinct d.session_id) as total + from website_event e + join session_data d final + on d.session_id = e.session_id + where e.website_id = {websiteId:UUID} + and e.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and d.data_key != '' ${filterQuery} - group by data_key + group by 1 order by 2 desc limit 500 `, diff --git a/src/queries/analytics/sessions/getSessionDataValues.ts b/src/queries/sql/sessions/getSessionDataValues.ts similarity index 66% rename from src/queries/analytics/sessions/getSessionDataValues.ts rename to src/queries/sql/sessions/getSessionDataValues.ts index c02e4adb..8cd6a4ab 100644 --- a/src/queries/analytics/sessions/getSessionDataValues.ts +++ b/src/queries/sql/sessions/getSessionDataValues.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { QueryFilters, WebsiteEventData } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import { QueryFilters, WebsiteEventData } from '@/lib/types'; export async function getSessionDataValues( ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }] @@ -27,11 +27,13 @@ async function relationalQuery( when data_type = 4 then ${getDateSQL('date_value', 'hour')} else string_value end as "value", - count(*) as "total" - from session_data - where website_id = {{websiteId::uuid}} - and created_at between {{startDate}} and {{endDate}} - and data_key = {{propertyName}} + count(distinct d.session_id) as "total" + from website_event e + join session_data d + on d.session_id = e.session_id + where e.website_id = {{websiteId::uuid}} + and e.created_at between {{startDate}} and {{endDate}} + and d.data_key = {{propertyName}} ${filterQuery} group by value order by 2 desc @@ -54,11 +56,13 @@ async function clickhouseQuery( multiIf(data_type = 2, replaceAll(string_value, '.0000', ''), data_type = 4, toString(date_trunc('hour', date_value)), string_value) as "value", - count(*) as "total" - from session_data final - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and data_key = {propertyName:String} + uniq(d.session_id) as "total" + from website_event e + join session_data d final + on d.session_id = e.session_id + where e.website_id = {websiteId:UUID} + and e.created_at between {startDate:DateTime64} and {endDate:DateTime64} + and d.data_key = {propertyName:String} ${filterQuery} group by value order by 2 desc diff --git a/src/queries/analytics/sessions/getSessionMetrics.ts b/src/queries/sql/sessions/getSessionMetrics.ts similarity index 83% rename from src/queries/analytics/sessions/getSessionMetrics.ts rename to src/queries/sql/sessions/getSessionMetrics.ts index bb8bc4c5..010989b5 100644 --- a/src/queries/analytics/sessions/getSessionMetrics.ts +++ b/src/queries/sql/sessions/getSessionMetrics.ts @@ -1,11 +1,17 @@ -import clickhouse from 'lib/clickhouse'; -import { EVENT_COLUMNS, EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_COLUMNS, EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; export async function getSessionMetrics( - ...args: [websiteId: string, type: string, filters: QueryFilters, limit?: number, offset?: number] + ...args: [ + websiteId: string, + type: string, + filters: QueryFilters, + limit?: number | string, + offset?: number | string, + ] ) { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -17,8 +23,8 @@ async function relationalQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ) { const column = FILTER_COLUMNS[type] || type; const { parseFilters, rawQuery } = prisma; @@ -60,8 +66,8 @@ async function clickhouseQuery( websiteId: string, type: string, filters: QueryFilters, - limit: number = 500, - offset: number = 0, + limit: number | string = 500, + offset: number | string = 0, ): Promise<{ x: string; y: number }[]> { const column = FILTER_COLUMNS[type] || type; const { parseFilters, rawQuery } = clickhouse; @@ -109,9 +115,5 @@ async function clickhouseQuery( `; } - return rawQuery(sql, params).then(a => { - return Object.values(a).map(a => { - return { x: a.x, y: Number(a.y), country: a.country }; - }); - }); + return rawQuery(sql, params); } diff --git a/src/queries/analytics/sessions/getSessionStats.ts b/src/queries/sql/sessions/getSessionStats.ts similarity index 91% rename from src/queries/analytics/sessions/getSessionStats.ts rename to src/queries/sql/sessions/getSessionStats.ts index 212f15e9..22cc04a7 100644 --- a/src/queries/analytics/sessions/getSessionStats.ts +++ b/src/queries/sql/sessions/getSessionStats.ts @@ -1,8 +1,8 @@ -import clickhouse from 'lib/clickhouse'; -import { EVENT_COLUMNS, EVENT_TYPE } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { EVENT_COLUMNS, EVENT_TYPE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; export async function getSessionStats(...args: [websiteId: string, filters: QueryFilters]) { return runQuery({ diff --git a/src/queries/analytics/sessions/getWebsiteSession.ts b/src/queries/sql/sessions/getWebsiteSession.ts similarity index 96% rename from src/queries/analytics/sessions/getWebsiteSession.ts rename to src/queries/sql/sessions/getWebsiteSession.ts index 2c16741e..45e8640a 100644 --- a/src/queries/analytics/sessions/getWebsiteSession.ts +++ b/src/queries/sql/sessions/getWebsiteSession.ts @@ -1,6 +1,6 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db'; export async function getWebsiteSession(...args: [websiteId: string, sessionId: string]) { return runQuery({ diff --git a/src/queries/analytics/sessions/getWebsiteSessionStats.ts b/src/queries/sql/sessions/getWebsiteSessionStats.ts similarity index 91% rename from src/queries/analytics/sessions/getWebsiteSessionStats.ts rename to src/queries/sql/sessions/getWebsiteSessionStats.ts index 648be140..2463b7ad 100644 --- a/src/queries/analytics/sessions/getWebsiteSessionStats.ts +++ b/src/queries/sql/sessions/getWebsiteSessionStats.ts @@ -1,7 +1,7 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters } from '@/lib/types'; export async function getWebsiteSessionStats( ...args: [websiteId: string, filters: QueryFilters] diff --git a/src/queries/analytics/sessions/getWebsiteSessions.ts b/src/queries/sql/sessions/getWebsiteSessions.ts similarity index 93% rename from src/queries/analytics/sessions/getWebsiteSessions.ts rename to src/queries/sql/sessions/getWebsiteSessions.ts index d2a827d0..264a084b 100644 --- a/src/queries/analytics/sessions/getWebsiteSessions.ts +++ b/src/queries/sql/sessions/getWebsiteSessions.ts @@ -1,7 +1,7 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; -import { PageParams, QueryFilters } from 'lib/types'; +import clickhouse from '@/lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { PageParams, QueryFilters } from '@/lib/types'; export async function getWebsiteSessions( ...args: [websiteId: string, filters?: QueryFilters, pageParams?: PageParams] diff --git a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts b/src/queries/sql/sessions/getWebsiteSessionsWeekly.ts similarity index 90% rename from src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts rename to src/queries/sql/sessions/getWebsiteSessionsWeekly.ts index 48d4f7a9..58f8d692 100644 --- a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts +++ b/src/queries/sql/sessions/getWebsiteSessionsWeekly.ts @@ -1,7 +1,7 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; -import { QueryFilters } from 'lib/types'; +import prisma from '@/lib/prisma'; +import clickhouse from '@/lib/clickhouse'; +import { runQuery, PRISMA, CLICKHOUSE } from '@/lib/db'; +import { QueryFilters } from '@/lib/types'; export async function getWebsiteSessionsWeekly( ...args: [websiteId: string, filters?: QueryFilters] diff --git a/src/queries/analytics/sessions/saveSessionData.ts b/src/queries/sql/sessions/saveSessionData.ts similarity index 84% rename from src/queries/analytics/sessions/saveSessionData.ts rename to src/queries/sql/sessions/saveSessionData.ts index f9f0276e..35f0c712 100644 --- a/src/queries/analytics/sessions/saveSessionData.ts +++ b/src/queries/sql/sessions/saveSessionData.ts @@ -1,11 +1,11 @@ -import { DATA_TYPE } from 'lib/constants'; -import { uuid } from 'lib/crypto'; -import { flattenJSON, getStringValue } from 'lib/data'; -import prisma from 'lib/prisma'; -import { DynamicData } from 'lib/types'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import kafka from 'lib/kafka'; -import clickhouse from 'lib/clickhouse'; +import { DATA_TYPE } from '@/lib/constants'; +import { uuid } from '@/lib/crypto'; +import { flattenJSON, getStringValue } from '@/lib/data'; +import prisma from '@/lib/prisma'; +import { DynamicData } from '@/lib/types'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import kafka from '@/lib/kafka'; +import clickhouse from '@/lib/clickhouse'; export async function saveSessionData(data: { websiteId: string; @@ -81,7 +81,7 @@ async function clickhouseQuery(data: { const { websiteId, sessionId, sessionData } = data; const { insert, getUTCString } = clickhouse; - const { sendMessages } = kafka; + const { sendMessage } = kafka; const createdAt = getUTCString(); const jsonKeys = flattenJSON(sessionData); @@ -100,7 +100,7 @@ async function clickhouseQuery(data: { }); if (kafka.enabled) { - await sendMessages('session_data', messages); + await sendMessage('session_data', messages); } else { await insert('session_data', messages); } diff --git a/src/store/app.ts b/src/store/app.ts index 4d547d4e..0890b7e9 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -7,9 +7,9 @@ import { LOCALE_CONFIG, THEME_CONFIG, TIMEZONE_CONFIG, -} from 'lib/constants'; -import { getItem } from 'next-basics'; -import { getTimezone } from 'lib/date'; +} from '@/lib/constants'; +import { getItem } from '@/lib/storage'; +import { getTimezone } from '@/lib/date'; function getDefaultTheme() { return typeof window !== 'undefined' diff --git a/src/store/dashboard.ts b/src/store/dashboard.ts index 0cfc78b9..a34ec384 100644 --- a/src/store/dashboard.ts +++ b/src/store/dashboard.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; -import { DASHBOARD_CONFIG, DEFAULT_WEBSITE_LIMIT } from 'lib/constants'; -import { getItem, setItem } from 'next-basics'; +import { DASHBOARD_CONFIG, DEFAULT_WEBSITE_LIMIT } from '@/lib/constants'; +import { getItem, setItem } from '@/lib/storage'; export const initialState = { showCharts: true, diff --git a/src/store/version.ts b/src/store/version.ts index 3b5afaac..9a889636 100644 --- a/src/store/version.ts +++ b/src/store/version.ts @@ -1,8 +1,8 @@ import { create } from 'zustand'; import { produce } from 'immer'; import semver from 'semver'; -import { CURRENT_VERSION, VERSION_CHECK, UPDATES_URL } from 'lib/constants'; -import { getItem } from 'next-basics'; +import { CURRENT_VERSION, VERSION_CHECK, UPDATES_URL } from '@/lib/constants'; +import { getItem } from '@/lib/storage'; const initialState = { current: CURRENT_VERSION, diff --git a/src/store/websites.ts b/src/store/websites.ts index 1c5c21fc..e9271abd 100644 --- a/src/store/websites.ts +++ b/src/store/websites.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; import { produce } from 'immer'; -import { DateRange } from 'lib/types'; +import { DateRange } from '@/lib/types'; const store = create(() => ({})); diff --git a/src/tracker/index.js b/src/tracker/index.js index 9fa560e6..dbd47b7c 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -5,6 +5,7 @@ location, document, history, + top, } = window; const { hostname, href, origin } = location; const { currentScript, referrer } = document; @@ -21,6 +22,7 @@ const tag = attr(_data + 'tag'); const autoTrack = attr(_data + 'auto-track') !== _false; const excludeSearch = attr(_data + 'exclude-search') === _true; + const excludeHash = attr(_data + 'exclude-hash') === _true; const domain = attr(_data + 'domains') || ''; const domains = domain.split(',').map(n => n.trim()); const host = @@ -33,43 +35,14 @@ /* Helper functions */ - const encode = str => { - if (!str) { - return undefined; - } - - try { - const result = decodeURI(str); - - if (result !== str) { - return result; - } - } catch (e) { - return str; - } - - return encodeURI(str); - }; - - const parseURL = url => { - try { - // use location.origin as the base to handle cases where the url is a relative path - const { pathname, search, hash } = new URL(url, location.href); - url = pathname + search + hash; - } catch (e) { - /* empty */ - } - return excludeSearch ? url.split('?')[0] : url; - }; - const getPayload = () => ({ website, - hostname, screen, language, - title: encode(title), - url: encode(currentUrl), - referrer: encode(currentRef), + title, + hostname, + url: currentUrl, + referrer: currentRef, tag: tag ? tag : undefined, }); @@ -79,7 +52,17 @@ if (!url) return; currentRef = currentUrl; - currentUrl = parseURL(url.toString()); + currentUrl = new URL(url, location.href); + + if (excludeSearch) { + currentUrl.search = ''; + } + + if (excludeHash) { + currentUrl.hash = ''; + } + + currentUrl = currentUrl.toString(); if (currentUrl !== currentRef) { setTimeout(track, delayDuration); @@ -176,7 +159,9 @@ e.preventDefault(); } return trackElement(parentElement).then(() => { - if (!external) location.href = href; + if (!external) { + (target === '_top' ? top.location : location).href = href; + } }); } } else if (parentElement.tagName === 'BUTTON') { @@ -194,6 +179,7 @@ /* Tracking functions */ const trackingDisabled = () => + disabled || !website || (localStorage && localStorage.getItem('umami.disabled')) || (domain && !domains.includes(hostname)); @@ -214,10 +200,15 @@ method: 'POST', body: JSON.stringify({ type, payload }), headers, + credentials: 'omit', }); - const text = await res.text(); - return (cache = text); + const data = await res.json(); + + if (data) { + disabled = !!data.disabled; + cache = data.cache; + } } catch (e) { /* empty */ } @@ -259,11 +250,12 @@ }; } - let currentUrl = parseURL(href); + let currentUrl = href; let currentRef = referrer.startsWith(origin) ? '' : referrer; let title = document.title; let cache; let initialized; + let disabled = false; if (autoTrack && !trackingDisabled()) { if (document.readyState === 'complete') { diff --git a/tsconfig.json b/tsconfig.json index 82e7166f..efe4861d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2021", + "target": "es2022", "outDir": "./build", "module": "esnext", "moduleResolution": "node", @@ -21,18 +21,10 @@ "noEmit": true, "jsx": "preserve", "incremental": false, - "baseUrl": "./src", "types": ["jest"], "typeRoots": ["node_modules/@types"], "paths": { - "react": ["./node_modules/@types/react"], - "assets/*": ["./assets/*"], - "components/*": ["./components/*"], - "lib/*": ["./lib/*"], - "pages/*": ["./pages/*"], - "queries/*": ["./queries/*"], - "store/*": ["./store/*"], - "styles/*": ["./styles/*"] + "@/*": ["./src/*"] }, "plugins": [ { diff --git a/yarn.lock b/yarn.lock index 03c84065..bf1a180e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1110,27 +1110,27 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d" - integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/runtime@^7.12.5": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.15.4", "@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.6": + version "7.26.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.7.tgz#f4e7fe527cd710f8dc0618610b61b4b060c3c341" + integrity sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.22.5": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -1204,17 +1204,17 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@clickhouse/client-common@1.8.1": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@clickhouse/client-common/-/client-common-1.8.1.tgz#1b7994990d867ba195e05a43e3413f4cf3b119cb" - integrity sha512-Z0R5zKaS3N35Op338WVRHIfoqDh9gotXZwekm0lbHQmwNaj3nY2iJ113dFYKjb1V+ESu+PvLEA//LJUGZyPQOg== +"@clickhouse/client-common@1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@clickhouse/client-common/-/client-common-1.10.1.tgz#570271f01c1b35b2de634f1710919e2b706301f0" + integrity sha512-Duh3cign2ChvXABpjVj9Hkz5y20Zf48OE0Y50S4qBVPdhI81S4Rh4MI/bEwvwMnzHubSkiEQ+VhC5HzV8ybnpg== -"@clickhouse/client@^1.4.1": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@clickhouse/client/-/client-1.8.1.tgz#2273f6b993d21351c32d4e2a6a1b35b0c05d435b" - integrity sha512-Ec0pCdwftIPD7hCxhOukHS0Zxr2tDc5mNAHBqkT3c0c6GO2WQdZkME9+EcfGcoF7+foUp82F5a0bPfSDDjfWmg== +"@clickhouse/client@^1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@clickhouse/client/-/client-1.10.1.tgz#eacad0cd5feff892a24f1367d565af7a04eabe08" + integrity sha512-Ot/6l4hFALK6NtZDS2UegukfRXWkkftWHCnzKUwanpOQ3Jd+RVKx5dxQreeBG5XcRjt1xyf5904PFjbCnaulXg== dependencies: - "@clickhouse/client-common" "1.8.1" + "@clickhouse/client-common" "1.10.1" "@colors/colors@1.5.0": version "1.5.0" @@ -1591,130 +1591,138 @@ dependencies: tslib "^2.4.0" -"@esbuild/android-arm64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" - integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA== +"@esbuild/aix-ppc64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" + integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== -"@esbuild/android-arm@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d" - integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A== +"@esbuild/android-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" + integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== -"@esbuild/android-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1" - integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww== +"@esbuild/android-arm@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" + integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== -"@esbuild/darwin-arm64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276" - integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg== +"@esbuild/android-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" + integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== -"@esbuild/darwin-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" - integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw== +"@esbuild/darwin-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" + integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== -"@esbuild/freebsd-arm64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2" - integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ== +"@esbuild/darwin-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" + integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== -"@esbuild/freebsd-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4" - integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ== +"@esbuild/freebsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" + integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== -"@esbuild/linux-arm64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" - integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg== +"@esbuild/freebsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" + integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== -"@esbuild/linux-arm@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a" - integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA== +"@esbuild/linux-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" + integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== -"@esbuild/linux-ia32@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a" - integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ== +"@esbuild/linux-arm@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" + integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== -"@esbuild/linux-loong64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72" - integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ== +"@esbuild/linux-ia32@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" + integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== -"@esbuild/linux-mips64el@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289" - integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A== +"@esbuild/linux-loong64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" + integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== -"@esbuild/linux-ppc64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7" - integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg== +"@esbuild/linux-mips64el@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" + integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== -"@esbuild/linux-riscv64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09" - integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA== +"@esbuild/linux-ppc64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" + integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== -"@esbuild/linux-s390x@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829" - integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q== +"@esbuild/linux-riscv64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" + integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== -"@esbuild/linux-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" - integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw== +"@esbuild/linux-s390x@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" + integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== -"@esbuild/netbsd-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462" - integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q== +"@esbuild/linux-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" + integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== -"@esbuild/openbsd-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691" - integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g== +"@esbuild/netbsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" + integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== -"@esbuild/sunos-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273" - integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg== +"@esbuild/netbsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" + integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== -"@esbuild/win32-arm64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f" - integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag== +"@esbuild/openbsd-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" + integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== -"@esbuild/win32-ia32@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03" - integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw== +"@esbuild/openbsd-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" + integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== -"@esbuild/win32-x64@0.17.19": - version "0.17.19" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" - integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA== +"@esbuild/sunos-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" + integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== -"@eslint-community/eslint-utils@^4.2.0": +"@esbuild/win32-arm64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" + integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== + +"@esbuild/win32-ia32@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" + integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== + +"@esbuild/win32-x64@0.25.0": + version "0.25.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" + integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ== + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA== dependencies: eslint-visitor-keys "^3.4.3" -"@eslint-community/eslint-utils@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== - dependencies: - eslint-visitor-keys "^3.3.0" - "@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": version "4.12.1" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" @@ -1803,6 +1811,16 @@ "@formatjs/intl-localematcher" "0.5.8" tslib "2" +"@formatjs/ecma402-abstract@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz#0ee291effe7ee2c340742a6c95d92eacb5e6c00a" + integrity sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg== + dependencies: + "@formatjs/fast-memoize" "2.2.6" + "@formatjs/intl-localematcher" "0.5.10" + decimal.js "10" + tslib "2" + "@formatjs/fast-memoize@2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz#74e64109279d5244f9fc281f3ae90c407cece823" @@ -1810,6 +1828,13 @@ dependencies: tslib "2" +"@formatjs/fast-memoize@2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz#fac0a84207a1396be1f1aa4ee2805b179e9343d1" + integrity sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw== + dependencies: + tslib "2" + "@formatjs/icu-messageformat-parser@2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz#a54293dd7f098d6a6f6a084ab08b6d54a3e8c12d" @@ -1819,6 +1844,15 @@ "@formatjs/icu-skeleton-parser" "1.3.6" tslib "^2.1.0" +"@formatjs/icu-messageformat-parser@2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.0.tgz#28d22a735114b7309c0d3e43d39f2660917867c8" + integrity sha512-Hp81uTjjdTk3FLh/dggU5NK7EIsVWc5/ZDWrIldmf2rBuPejuZ13CZ/wpVE2SToyi4EiroPTQ1XJcJuZFIxTtw== + dependencies: + "@formatjs/ecma402-abstract" "2.3.2" + "@formatjs/icu-skeleton-parser" "1.8.12" + tslib "2" + "@formatjs/icu-messageformat-parser@2.9.4": version "2.9.4" resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz#52501fbdc122a86097644f03ae1117b9ced00872" @@ -1836,6 +1870,14 @@ "@formatjs/ecma402-abstract" "1.11.4" tslib "^2.1.0" +"@formatjs/icu-skeleton-parser@1.8.12": + version "1.8.12" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.12.tgz#43076747cdbe0f23bfac2b2a956bd8219716680d" + integrity sha512-QRAY2jC1BomFQHYDMcZtClqHR55EEnB96V7Xbk/UiBodsuFc5kujybzt87+qj1KqmJozFhk6n4KiT1HKwAkcfg== + dependencies: + "@formatjs/ecma402-abstract" "2.3.2" + tslib "2" + "@formatjs/icu-skeleton-parser@1.8.8": version "1.8.8" resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz#a16eff7fd040acf096fb1853c99527181d38cf90" @@ -1869,6 +1911,13 @@ dependencies: tslib "^2.1.0" +"@formatjs/intl-localematcher@0.5.10": + version "0.5.10" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz#1e0bd3fc1332c1fe4540cfa28f07e9227b659a58" + integrity sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q== + dependencies: + tslib "2" + "@formatjs/intl-localematcher@0.5.8": version "0.5.8" resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz#b11bbd04bd3551f7cadcb1ef1e231822d0e3c97e" @@ -1897,6 +1946,17 @@ intl-messageformat "10.7.7" tslib "2" +"@formatjs/intl@3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-3.1.3.tgz#457dba081679a5e899c900624331608742df138e" + integrity sha512-yWtB1L4vOr17MLII3bcNRmjx6qBkSupJuA6nJz1zVxpWbJXKQL5vgvjRCehTO3z7HolxFjtLdfV0/RN+bC34Fg== + dependencies: + "@formatjs/ecma402-abstract" "2.3.2" + "@formatjs/fast-memoize" "2.2.6" + "@formatjs/icu-messageformat-parser" "2.11.0" + intl-messageformat "10.7.14" + tslib "2" + "@formatjs/ts-transformer@3.9.4": version "3.9.4" resolved "https://registry.yarnpkg.com/@formatjs/ts-transformer/-/ts-transformer-3.9.4.tgz#14b43628d082cb8cd8bc15c4893197b59903ec2c" @@ -1917,6 +1977,19 @@ tslib "^2.0.1" typescript "^4.0" +"@hello-pangea/dnd@^17.0.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-17.0.0.tgz#2dede20fd6d8a9b53144547e6894fc482da3d431" + integrity sha512-LDDPOix/5N0j5QZxubiW9T0M0+1PR0rTDWeZF5pu1Tz91UQnuVK4qQ/EjY83Qm2QeX0eM8qDXANfDh3VVqtR4Q== + dependencies: + "@babel/runtime" "^7.25.6" + css-box-model "^1.2.1" + memoize-one "^6.0.0" + raf-schd "^4.0.3" + react-redux "^9.1.2" + redux "^5.0.1" + use-memo-one "^1.1.3" + "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" @@ -2049,6 +2122,18 @@ resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342" integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -2314,24 +2399,24 @@ "@jridgewell/sourcemap-codec" "^1.4.14" "@kurkle/color@^0.3.0": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" - integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== + version "0.3.4" + resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf" + integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w== "@netlify/plugin-nextjs@^5.8.1": - version "5.8.1" - resolved "https://registry.yarnpkg.com/@netlify/plugin-nextjs/-/plugin-nextjs-5.8.1.tgz#9da15bb4a13c5644e9b58b968c7da51939206ee4" - integrity sha512-WB1N0FslhWZ1yAVYTcB6CcFrFOUSQ0O2LfavYZrbAypeNxu2I+oO+cgmhfDgZ8Eoq1g4EMeoIGMkNoZ4ogZTsg== + version "5.9.3" + resolved "https://registry.yarnpkg.com/@netlify/plugin-nextjs/-/plugin-nextjs-5.9.3.tgz#164d20dba170a5ef2531e24f5cd9f9ea1b6a4eed" + integrity sha512-760YvPaw6tSPmmX/2sqRzqBehPOfTD6lJoUb+bW+yZhQNyfLyCJR9/cRQP0+fFN2CCmdRWECX4nbi76Qne5/FQ== "@next/env@15.0.4": version "15.0.4" resolved "https://registry.yarnpkg.com/@next/env/-/env-15.0.4.tgz#97da0fe3bae2f2b2968c4c925d7936660f5b3836" integrity sha512-WNRvtgnRVDD4oM8gbUcRc27IAhaL4eXQ/2ovGbgLnPGUvdyDr8UdXP4Q/IBDdAdojnD2eScryIDirv0YUCjUVw== -"@next/eslint-plugin-next@14.2.18": - version "14.2.18" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.18.tgz#602d2b1b1083e3d290116beb6d340e00930e63ab" - integrity sha512-KyYTbZ3GQwWOjX3Vi1YcQbekyGP0gdammb7pbmmi25HBUCINzDReyrzCMOJIeZisK1Q3U6DT5Rlc4nm2/pQeXA== +"@next/eslint-plugin-next@14.2.23": + version "14.2.23" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.23.tgz#b7903c7a99108e73d318fadb5c76de3cc2c58ab4" + integrity sha512-efRC7m39GoiU1fXZRgGySqYbQi6ZyLkuGlvGst7IwkTTczehQTJA/7PoMg4MMjUZvZEGpiSEu+oJBAjPawiC3Q== dependencies: glob "10.3.10" @@ -2406,51 +2491,56 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@prisma/client@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.22.0.tgz#da1ca9c133fbefe89e0da781c75e1c59da5f8802" - integrity sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA== +"@prisma/client@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-6.1.0.tgz#179d3b70586e7be522f6f1f0a82cca01396f719a" + integrity sha512-AbQYc5+EJKm1Ydfq3KxwcGiy7wIbm4/QbjCKWWoNROtvy7d6a3gmAGkKjK0iUCzh+rHV8xDhD5Cge8ke/kiy5Q== -"@prisma/debug@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.22.0.tgz#58af56ed7f6f313df9fb1042b6224d3174bbf412" - integrity sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ== +"@prisma/debug@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-6.1.0.tgz#a27a1d144f72a3bc95061ecb0255e7554d9d59ec" + integrity sha512-0himsvcM4DGBTtvXkd2Tggv6sl2JyUYLzEGXXleFY+7Kp6rZeSS3hiTW9mwtUlXrwYbJP6pwlVNB7jYElrjWUg== -"@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2": - version "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz#d534dd7235c1ba5a23bacd5b92cc0ca3894c28f4" - integrity sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ== +"@prisma/engines-version@6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959": + version "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959.tgz#0b21ebf57362ffe35d0760c39855f90bbfa0f2fd" + integrity sha512-PdJqmYM2Fd8K0weOOtQThWylwjsDlTig+8Pcg47/jszMuLL9iLIaygC3cjWJLda69siRW4STlCTMSgOjZzvKPQ== -"@prisma/engines@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.22.0.tgz#28f3f52a2812c990a8b66eb93a0987816a5b6d84" - integrity sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA== +"@prisma/engines@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-6.1.0.tgz#2195244a8ce33839a8131e4465624e21d1f8d042" + integrity sha512-GnYJbCiep3Vyr1P/415ReYrgJUjP79fBNc1wCo7NP6Eia0CzL2Ot9vK7Infczv3oK7JLrCcawOSAxFxNFsAERQ== dependencies: - "@prisma/debug" "5.22.0" - "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" - "@prisma/fetch-engine" "5.22.0" - "@prisma/get-platform" "5.22.0" + "@prisma/debug" "6.1.0" + "@prisma/engines-version" "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959" + "@prisma/fetch-engine" "6.1.0" + "@prisma/get-platform" "6.1.0" "@prisma/extension-read-replicas@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@prisma/extension-read-replicas/-/extension-read-replicas-0.3.0.tgz#2842a7c928f957c1dd58a6256104797596d43426" integrity sha512-F9+rSmYday6GT2qjhJtkZcBOpLO5LtpvFcMGqrBDHf+78LEdSuxfFjOxYlNuqk4B+th62yxpbhfpmB9/Mca14Q== -"@prisma/fetch-engine@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz#4fb691b483a450c5548aac2f837b267dd50ef52e" - integrity sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA== - dependencies: - "@prisma/debug" "5.22.0" - "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" - "@prisma/get-platform" "5.22.0" +"@prisma/extension-read-replicas@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@prisma/extension-read-replicas/-/extension-read-replicas-0.4.0.tgz#b83e829d881e86a165bdc30040434723a665332a" + integrity sha512-0KVjOgYJO9ymOACwLXebXB1XZCrfXXZMDkZrdZQOm9rqraC1rguwpPzXUdgv2txMLyJNSe+i5DOruDCU4FfvCA== -"@prisma/get-platform@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.22.0.tgz#fc675bc9d12614ca2dade0506c9c4a77e7dddacd" - integrity sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q== +"@prisma/fetch-engine@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-6.1.0.tgz#2a5174787bf57c9b1d5d400bb923e0dc6a73a794" + integrity sha512-asdFi7TvPlEZ8CzSZ/+Du5wZ27q6OJbRSXh+S8ISZguu+S9KtS/gP7NeXceZyb1Jv1SM1S5YfiCv+STDsG6rrg== dependencies: - "@prisma/debug" "5.22.0" + "@prisma/debug" "6.1.0" + "@prisma/engines-version" "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959" + "@prisma/get-platform" "6.1.0" + +"@prisma/get-platform@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-6.1.0.tgz#d4394a24ef91af6675a92382ed40e6e6e07eeb13" + integrity sha512-ia8bNjboBoHkmKGGaWtqtlgQOhCi7+f85aOkPJKgNwWvYrT6l78KgojLekE8zMhVk0R9lWcifV0Pf8l3/15V0Q== + dependencies: + "@prisma/debug" "6.1.0" "@react-spring/animated@~9.7.5": version "9.7.5" @@ -2556,9 +2646,9 @@ "@rollup/pluginutils" "^5.1.0" "@rollup/plugin-node-resolve@^15.2.0": - version "15.3.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz#efbb35515c9672e541c08d59caba2eff492a55d5" - integrity sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag== + version "15.3.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz#66008953c2524be786aa319d49e32f2128296a78" + integrity sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA== dependencies: "@rollup/pluginutils" "^5.0.1" "@types/resolve" "1.20.2" @@ -2575,9 +2665,9 @@ magic-string "^0.30.3" "@rollup/pluginutils@^5.0.1": - version "5.1.3" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.3.tgz#3001bf1a03f3ad24457591f2c259c8e514e0dbdf" - integrity sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A== + version "5.1.4" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.4.tgz#bb94f1f9eaaac944da237767cdfee6c5b2262d4a" + integrity sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ== dependencies: "@types/estree" "^1.0.0" estree-walker "^2.0.2" @@ -2607,9 +2697,9 @@ integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== "@rushstack/eslint-patch@^1.3.3": - version "1.10.4" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1" - integrity sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA== + version "1.10.5" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.5.tgz#3a1c12c959010a55c17d46b395ed3047b545c246" + integrity sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A== "@sinclair/typebox@^0.27.8": version "0.27.8" @@ -2763,17 +2853,17 @@ dependencies: tslib "^2.4.0" -"@tanstack/query-core@5.60.6": - version "5.60.6" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.60.6.tgz#0dd33fe231b0d18bf66d0c615b29899738300658" - integrity sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ== +"@tanstack/query-core@5.62.16": + version "5.62.16" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.62.16.tgz#f7efc92b1562a054748bc00c7f8d9d833407503b" + integrity sha512-9Sgft7Qavcd+sN0V25xVyo0nfmcZXBuODy3FVG7BMWTg1HMLm8wwG5tNlLlmSic1u7l1v786oavn+STiFaPH2g== "@tanstack/react-query@^5.28.6": - version "5.61.0" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.61.0.tgz#73473feb37aa28ceb410e297ee060e18f06f88e0" - integrity sha512-SBzV27XAeCRBOQ8QcC94w2H1Md0+LI0gTWwc3qRJoaGuewKn5FNW4LSqwPFJZVEItfhMfGT7RpZuSFXjTi12pQ== + version "5.63.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.63.0.tgz#18e00e24a485f707cfd41f4b30679f70e6ca6016" + integrity sha512-QWizLzSiog8xqIRYmuJRok9VELlXVBAwtINgVCgW1SNvamQwWDO5R0XFSkjoBEj53x9Of1KAthLRBUC5xmtVLQ== dependencies: - "@tanstack/query-core" "5.60.6" + "@tanstack/query-core" "5.62.16" "@trysound/sax@0.2.0": version "0.2.0" @@ -2910,14 +3000,6 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" -"@types/hoist-non-react-statics@^3.3.0": - version "3.3.4" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.4.tgz#cc477ce0283bb9d19ea0cbfa2941fe2c8493a1be" - integrity sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" @@ -2965,11 +3047,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@^4.14.175": - version "4.14.200" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.200.tgz#435b6035c7eba9cdf1e039af8212c9e9281e7149" - integrity sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q== - "@types/minimatch@*": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" @@ -3002,12 +3079,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== -"@types/node@^20.9.0": - version "20.17.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.6.tgz#6e4073230c180d3579e8c60141f99efdf5df0081" - integrity sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ== +"@types/node@^22.13.4": + version "22.13.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.4.tgz#3fe454d77cd4a2d73c214008b3e331bfaaf5038a" + integrity sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg== dependencies: - undici-types "~6.19.2" + undici-types "~6.20.0" "@types/normalize-package-data@^2.4.0": version "2.4.3" @@ -3015,26 +3092,21 @@ integrity sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg== "@types/prop-types@*": - version "15.7.13" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" - integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== + version "15.7.14" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" + integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== -"@types/react-dom@^18.2.17": - version "18.3.1" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" - integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== - dependencies: - "@types/react" "*" +"@types/react-dom@^19.0.2": + version "19.0.2" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.2.tgz#ad21f9a1ee881817995fd3f7fd33659c87e7b1b7" + integrity sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg== -"@types/react-redux@^7.1.20": - version "7.1.28" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.28.tgz#30a44303c7daceb6ede9cfb4aaf72e64f1dde4de" - integrity sha512-EQr7cChVzVUuqbA+J8ArWK1H0hLAHKOs21SIMrskKZ3nHNeE+LFYA+IsoZGhVOT8Ktjn3M20v4rnZKN3fLbypw== +"@types/react-intl@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/react-intl/-/react-intl-3.0.0.tgz#a2cce0024b6cfe403be28ccf67f49d720fa810ec" + integrity sha512-k8F3d05XQGEqSWIfK97bBjZe4z9RruXU9Wa7OZ2iUC5pdeIpzuQDZe/9C2J3Xir5//ZtAkhcv08Wfx3n5TBTQg== dependencies: - "@types/hoist-non-react-statics" "^3.3.0" - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" + react-intl "*" "@types/react-window@^1.8.8": version "1.8.8" @@ -3043,7 +3115,14 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@16 || 17 || 18", "@types/react@^18.2.41": +"@types/react@*": + version "19.0.4" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.4.tgz#ad1270e090118ac3c5f0928a29fe0ddf164881df" + integrity sha512-3O4QisJDYr1uTUMZHA2YswiQZRq+Pd8D+GdVFYikTutYsTz+QZgWkAPnP7rx9txoI6EXKcPiluMqWPFV3tT9Wg== + dependencies: + csstype "^3.0.2" + +"@types/react@16 || 17 || 18": version "18.3.12" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.12.tgz#99419f182ccd69151813b7ee24b792fe08774f60" integrity sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw== @@ -3051,6 +3130,13 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/react@16 || 17 || 18 || 19", "@types/react@^19.0.8": + version "19.0.8" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.8.tgz#7098e6159f2a61e4f4cef2c1223c044a9bec590e" + integrity sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw== + dependencies: + csstype "^3.0.2" + "@types/resolve@1.20.2": version "1.20.2" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" @@ -3083,6 +3169,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/use-sync-external-store@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" + integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -3103,19 +3194,19 @@ "@types/node" "*" "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz#c95c6521e70c8b095a684d884d96c0c1c63747d2" - integrity sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg== + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz#5f26c0a833b27bcb1aa402b82e76d3b8dda0b247" + integrity sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.15.0" - "@typescript-eslint/type-utils" "8.15.0" - "@typescript-eslint/utils" "8.15.0" - "@typescript-eslint/visitor-keys" "8.15.0" + "@typescript-eslint/scope-manager" "8.19.1" + "@typescript-eslint/type-utils" "8.19.1" + "@typescript-eslint/utils" "8.19.1" + "@typescript-eslint/visitor-keys" "8.19.1" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" - ts-api-utils "^1.3.0" + ts-api-utils "^2.0.0" "@typescript-eslint/eslint-plugin@^6.7.3": version "6.21.0" @@ -3135,14 +3226,14 @@ ts-api-utils "^1.0.1" "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.15.0.tgz#92610da2b3af702cfbc02a46e2a2daa6260a9045" - integrity sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A== + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.19.1.tgz#b836fcfe7a704c8c65f5a50e5b0ff8acfca5c21b" + integrity sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw== dependencies: - "@typescript-eslint/scope-manager" "8.15.0" - "@typescript-eslint/types" "8.15.0" - "@typescript-eslint/typescript-estree" "8.15.0" - "@typescript-eslint/visitor-keys" "8.15.0" + "@typescript-eslint/scope-manager" "8.19.1" + "@typescript-eslint/types" "8.19.1" + "@typescript-eslint/typescript-estree" "8.19.1" + "@typescript-eslint/visitor-keys" "8.19.1" debug "^4.3.4" "@typescript-eslint/parser@^6.7.3": @@ -3172,13 +3263,13 @@ "@typescript-eslint/types" "6.21.0" "@typescript-eslint/visitor-keys" "6.21.0" -"@typescript-eslint/scope-manager@8.15.0": - version "8.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz#28a1a0f13038f382424f45a988961acaca38f7c6" - integrity sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA== +"@typescript-eslint/scope-manager@8.19.1": + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz#794cfc8add4f373b9cd6fa32e367e7565a0e231b" + integrity sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q== dependencies: - "@typescript-eslint/types" "8.15.0" - "@typescript-eslint/visitor-keys" "8.15.0" + "@typescript-eslint/types" "8.19.1" + "@typescript-eslint/visitor-keys" "8.19.1" "@typescript-eslint/type-utils@6.21.0": version "6.21.0" @@ -3190,15 +3281,15 @@ debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/type-utils@8.15.0": - version "8.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz#a6da0f93aef879a68cc66c73fe42256cb7426c72" - integrity sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw== +"@typescript-eslint/type-utils@8.19.1": + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz#23710ab52643c19f74601b3f4a076c98f4e159aa" + integrity sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw== dependencies: - "@typescript-eslint/typescript-estree" "8.15.0" - "@typescript-eslint/utils" "8.15.0" + "@typescript-eslint/typescript-estree" "8.19.1" + "@typescript-eslint/utils" "8.19.1" debug "^4.3.4" - ts-api-utils "^1.3.0" + ts-api-utils "^2.0.0" "@typescript-eslint/types@5.62.0": version "5.62.0" @@ -3210,10 +3301,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== -"@typescript-eslint/types@8.15.0": - version "8.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.15.0.tgz#4958edf3d83e97f77005f794452e595aaf6430fc" - integrity sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ== +"@typescript-eslint/types@8.19.1": + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.19.1.tgz#015a991281754ed986f2e549263a1188d6ed0a8c" + integrity sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" @@ -3242,19 +3333,19 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/typescript-estree@8.15.0": - version "8.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz#915c94e387892b114a2a2cc0df2d7f19412c8ba7" - integrity sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg== +"@typescript-eslint/typescript-estree@8.19.1": + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz#c1094bb00bc251ac76cf215569ca27236435036b" + integrity sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q== dependencies: - "@typescript-eslint/types" "8.15.0" - "@typescript-eslint/visitor-keys" "8.15.0" + "@typescript-eslint/types" "8.19.1" + "@typescript-eslint/visitor-keys" "8.19.1" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" minimatch "^9.0.4" semver "^7.6.0" - ts-api-utils "^1.3.0" + ts-api-utils "^2.0.0" "@typescript-eslint/utils@6.21.0": version "6.21.0" @@ -3269,15 +3360,15 @@ "@typescript-eslint/typescript-estree" "6.21.0" semver "^7.5.4" -"@typescript-eslint/utils@8.15.0": - version "8.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.15.0.tgz#ac04679ad19252776b38b81954b8e5a65567cef6" - integrity sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ== +"@typescript-eslint/utils@8.19.1": + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.19.1.tgz#dd8eabd46b92bf61e573286e1c0ba6bd243a185b" + integrity sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.15.0" - "@typescript-eslint/types" "8.15.0" - "@typescript-eslint/typescript-estree" "8.15.0" + "@typescript-eslint/scope-manager" "8.19.1" + "@typescript-eslint/types" "8.19.1" + "@typescript-eslint/typescript-estree" "8.19.1" "@typescript-eslint/utils@^5.10.0": version "5.62.0" @@ -3309,12 +3400,12 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" -"@typescript-eslint/visitor-keys@8.15.0": - version "8.15.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz#9ea5a85eb25401d2aa74ec8a478af4e97899ea12" - integrity sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q== +"@typescript-eslint/visitor-keys@8.19.1": + version "8.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz#fce54d7cfa5351a92387d6c0c5be598caee072e0" + integrity sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q== dependencies: - "@typescript-eslint/types" "8.15.0" + "@typescript-eslint/types" "8.19.1" eslint-visitor-keys "^4.2.0" "@umami/prisma-client@^0.14.0": @@ -3326,10 +3417,10 @@ chalk "^4.1.2" debug "^4.3.4" -"@umami/redis-client@^0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@umami/redis-client/-/redis-client-0.24.0.tgz#8af489250396be76bc0906766343620589774c4b" - integrity sha512-yUZmC87H5QZKNA6jD9k/7d8WDaXQTDROlpyK7S+V2csD96eAnMNi7JsWAVWx9T/584QKD8DsSIy87PTWq1HNPw== +"@umami/redis-client@^0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@umami/redis-client/-/redis-client-0.26.0.tgz#0476e903a30322a43247dc292003224686971c12" + integrity sha512-j2vxb1gYF5zfk7BkrHgau2MwKsB5ijbQh2w1WoIvbP41cqTMsFm/zUrjhZ0cP1ZxR/riQR1AWxKmqNggYRZ5eA== dependencies: debug "^4.3.4" redis "^4.5.1" @@ -3589,13 +3680,13 @@ aria-query@^5.3.2: resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== -array-buffer-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" - integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== +array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== dependencies: - call-bind "^1.0.5" - is-array-buffer "^3.0.4" + call-bound "^1.0.3" + is-array-buffer "^3.0.5" array-find-index@^1.0.1: version "1.0.2" @@ -3644,24 +3735,24 @@ array.prototype.findlastindex@^1.2.5: es-shim-unscopables "^1.0.2" array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" - integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" + integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" -array.prototype.flatmap@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" - integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== +array.prototype.flatmap@^1.3.2, array.prototype.flatmap@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" array.prototype.tosorted@^1.1.4: version "1.1.4" @@ -3674,19 +3765,18 @@ array.prototype.tosorted@^1.1.4: es-errors "^1.3.0" es-shim-unscopables "^1.0.2" -arraybuffer.prototype.slice@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" - integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== dependencies: array-buffer-byte-length "^1.0.1" - call-bind "^1.0.5" + call-bind "^1.0.8" define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.2.1" - get-intrinsic "^1.2.3" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" is-array-buffer "^3.0.4" - is-shared-array-buffer "^1.0.2" arrify@^1.0.1: version "1.0.1" @@ -4002,16 +4092,31 @@ cachedir@^2.3.0: resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d" integrity sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ== -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" + integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== dependencies: - es-define-property "^1.0.0" es-errors "^1.3.0" function-bind "^1.1.2" + +call-bind@^1.0.2, call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" get-intrinsic "^1.2.4" - set-function-length "^1.2.1" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" + integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== + dependencies: + call-bind-apply-helpers "^1.0.1" + get-intrinsic "^1.2.6" callsites@^3.0.0: version "3.1.0" @@ -4100,9 +4205,9 @@ charenc@0.0.2: integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== chart.js@^4.4.2: - version "4.4.6" - resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.6.tgz#da39b84ca752298270d4c0519675c7659936abec" - integrity sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA== + version "4.4.7" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.7.tgz#7a01ee0b4dac3c03f2ab0589af888db296d896fa" + integrity sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw== dependencies: "@kurkle/color" "^0.3.0" @@ -4424,7 +4529,7 @@ css-blank-pseudo@^3.0.3: dependencies: postcss-selector-parser "^6.0.9" -css-box-model@^1.2.0: +css-box-model@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== @@ -4736,30 +4841,30 @@ data-uri-to-buffer@^4.0.0: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== -data-view-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" - integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== +data-view-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== dependencies: - call-bind "^1.0.6" + call-bound "^1.0.3" es-errors "^1.3.0" - is-data-view "^1.0.1" + is-data-view "^1.0.2" -data-view-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" - integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== +data-view-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" es-errors "^1.3.0" - is-data-view "^1.0.1" + is-data-view "^1.0.2" -data-view-byte-offset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" - integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== +data-view-byte-offset@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== dependencies: - call-bind "^1.0.6" + call-bound "^1.0.2" es-errors "^1.3.0" is-data-view "^1.0.1" @@ -4799,13 +4904,20 @@ debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: +debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: ms "^2.1.3" +debug@^4.3.4, debug@^4.3.7: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -4824,6 +4936,11 @@ decamelize@^5.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9" integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA== +decimal.js@10: + version "10.5.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.5.0.tgz#0f371c7cf6c4898ce0afb09836db73cd82010f22" + integrity sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw== + dedent@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" @@ -5009,6 +5126,15 @@ dotenv@^10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== +dunder-proto@^1.0.0, dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -5069,9 +5195,9 @@ end-of-stream@^1.1.0: once "^1.4.0" enhanced-resolve@^5.15.0: - version "5.17.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" - integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + version "5.18.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz#91eb1db193896b9801251eeff1c6980278b1e404" + integrity sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -5101,90 +5227,94 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: - version "1.23.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.5.tgz#f4599a4946d57ed467515ed10e4f157289cd52fb" - integrity sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ== +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9: + version "1.23.9" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.9.tgz#5b45994b7de78dada5c1bebf1379646b32b9d606" + integrity sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA== dependencies: - array-buffer-byte-length "^1.0.1" - arraybuffer.prototype.slice "^1.0.3" + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - data-view-buffer "^1.0.1" - data-view-byte-length "^1.0.1" - data-view-byte-offset "^1.0.0" - es-define-property "^1.0.0" + call-bind "^1.0.8" + call-bound "^1.0.3" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" es-errors "^1.3.0" es-object-atoms "^1.0.0" - es-set-tostringtag "^2.0.3" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.6" - get-intrinsic "^1.2.4" - get-symbol-description "^1.0.2" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.2.7" + get-proto "^1.0.0" + get-symbol-description "^1.1.0" globalthis "^1.0.4" - gopd "^1.0.1" + gopd "^1.2.0" has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" + has-proto "^1.2.0" + has-symbols "^1.1.0" hasown "^2.0.2" - internal-slot "^1.0.7" - is-array-buffer "^3.0.4" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" is-callable "^1.2.7" - is-data-view "^1.0.1" - is-negative-zero "^2.0.3" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.3" - is-string "^1.0.7" - is-typed-array "^1.1.13" - is-weakref "^1.0.2" + is-data-view "^1.0.2" + is-regex "^1.2.1" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.0" + math-intrinsics "^1.1.0" object-inspect "^1.13.3" object-keys "^1.1.1" - object.assign "^4.1.5" + object.assign "^4.1.7" + own-keys "^1.0.1" regexp.prototype.flags "^1.5.3" - safe-array-concat "^1.1.2" - safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.9" - string.prototype.trimend "^1.0.8" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" string.prototype.trimstart "^1.0.8" - typed-array-buffer "^1.0.2" - typed-array-byte-length "^1.0.1" - typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.6" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.15" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.18" -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== -es-errors@^1.2.1, es-errors@^1.3.0: +es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-iterator-helpers@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz#2f1a3ab998b30cb2d10b195b587c6d9ebdebf152" - integrity sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q== +es-iterator-helpers@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz#d1dd0f58129054c0ad922e6a9a1e65eef435fe75" + integrity sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" - es-abstract "^1.23.3" + es-abstract "^1.23.6" es-errors "^1.3.0" es-set-tostringtag "^2.0.3" function-bind "^1.1.2" - get-intrinsic "^1.2.4" + get-intrinsic "^1.2.6" globalthis "^1.0.4" - gopd "^1.0.1" + gopd "^1.2.0" has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" - internal-slot "^1.0.7" - iterator.prototype "^1.1.3" - safe-array-concat "^1.1.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + iterator.prototype "^1.1.4" + safe-array-concat "^1.1.3" es-module-lexer@^1.0.5: version "1.3.1" @@ -5198,58 +5328,62 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.0" -es-set-tostringtag@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" - integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== +es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== dependencies: - get-intrinsic "^1.2.4" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" has-tostringtag "^1.0.2" - hasown "^2.0.1" + hasown "^2.0.2" -es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: +es-shim-unscopables@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== dependencies: hasown "^2.0.0" -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" -esbuild@^0.17.17: - version "0.17.19" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955" - integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw== +esbuild@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92" + integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw== optionalDependencies: - "@esbuild/android-arm" "0.17.19" - "@esbuild/android-arm64" "0.17.19" - "@esbuild/android-x64" "0.17.19" - "@esbuild/darwin-arm64" "0.17.19" - "@esbuild/darwin-x64" "0.17.19" - "@esbuild/freebsd-arm64" "0.17.19" - "@esbuild/freebsd-x64" "0.17.19" - "@esbuild/linux-arm" "0.17.19" - "@esbuild/linux-arm64" "0.17.19" - "@esbuild/linux-ia32" "0.17.19" - "@esbuild/linux-loong64" "0.17.19" - "@esbuild/linux-mips64el" "0.17.19" - "@esbuild/linux-ppc64" "0.17.19" - "@esbuild/linux-riscv64" "0.17.19" - "@esbuild/linux-s390x" "0.17.19" - "@esbuild/linux-x64" "0.17.19" - "@esbuild/netbsd-x64" "0.17.19" - "@esbuild/openbsd-x64" "0.17.19" - "@esbuild/sunos-x64" "0.17.19" - "@esbuild/win32-arm64" "0.17.19" - "@esbuild/win32-ia32" "0.17.19" - "@esbuild/win32-x64" "0.17.19" + "@esbuild/aix-ppc64" "0.25.0" + "@esbuild/android-arm" "0.25.0" + "@esbuild/android-arm64" "0.25.0" + "@esbuild/android-x64" "0.25.0" + "@esbuild/darwin-arm64" "0.25.0" + "@esbuild/darwin-x64" "0.25.0" + "@esbuild/freebsd-arm64" "0.25.0" + "@esbuild/freebsd-x64" "0.25.0" + "@esbuild/linux-arm" "0.25.0" + "@esbuild/linux-arm64" "0.25.0" + "@esbuild/linux-ia32" "0.25.0" + "@esbuild/linux-loong64" "0.25.0" + "@esbuild/linux-mips64el" "0.25.0" + "@esbuild/linux-ppc64" "0.25.0" + "@esbuild/linux-riscv64" "0.25.0" + "@esbuild/linux-s390x" "0.25.0" + "@esbuild/linux-x64" "0.25.0" + "@esbuild/netbsd-arm64" "0.25.0" + "@esbuild/netbsd-x64" "0.25.0" + "@esbuild/openbsd-arm64" "0.25.0" + "@esbuild/openbsd-x64" "0.25.0" + "@esbuild/sunos-x64" "0.25.0" + "@esbuild/win32-arm64" "0.25.0" + "@esbuild/win32-ia32" "0.25.0" + "@esbuild/win32-x64" "0.25.0" escalade@^3.1.1: version "3.1.1" @@ -5272,11 +5406,11 @@ escape-string-regexp@^4.0.0: integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== eslint-config-next@^14.0.4: - version "14.2.18" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.2.18.tgz#e689e348a1db4bc563580cf77705eec894eaece1" - integrity sha512-SuDRcpJY5VHBkhz5DijJ4iA4bVnBA0n48Rb+YSJSCDr+h7kKAcb1mZHusLbW+WA8LDB6edSolomXA55eG3eOVA== + version "14.2.23" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.2.23.tgz#5639fe1c01bb7d5a6486a34a16fc37e0a0b603f8" + integrity sha512-qtWJzOsDZxnLtXLNtnVjbutHmnEp6QTTSZBTlTCge/Wy0AsUaq8nwR91dBcZZvFg3eY3zKFPBhUkLMHu3Qpauw== dependencies: - "@next/eslint-plugin-next" "14.2.18" + "@next/eslint-plugin-next" "14.2.23" "@rushstack/eslint-patch" "^1.3.3" "@typescript-eslint/eslint-plugin" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" "@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" @@ -5307,20 +5441,20 @@ eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: resolve "^1.22.4" eslint-import-resolver-typescript@^3.5.2: - version "3.6.3" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz#bb8e388f6afc0f940ce5d2c5fd4a3d147f038d9e" - integrity sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA== + version "3.7.0" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz#e69925936a771a9cb2de418ccebc4cdf6c0818aa" + integrity sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow== dependencies: "@nolyfill/is-core-module" "1.0.39" - debug "^4.3.5" + debug "^4.3.7" enhanced-resolve "^5.15.0" - eslint-module-utils "^2.8.1" fast-glob "^3.3.2" get-tsconfig "^4.7.5" is-bun-module "^1.0.2" is-glob "^4.0.3" + stable-hash "^0.0.4" -eslint-module-utils@^2.12.0, eslint-module-utils@^2.8.1: +eslint-module-utils@^2.12.0: version "2.12.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== @@ -5413,27 +5547,27 @@ eslint-plugin-promise@^6.1.1: integrity sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw== eslint-plugin-react@^7.33.2: - version "7.37.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz#cd0935987876ba2900df2f58339f6d92305acc7a" - integrity sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w== + version "7.37.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz#567549e9251533975c4ea9706f986c3a64832031" + integrity sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA== dependencies: array-includes "^3.1.8" array.prototype.findlast "^1.2.5" - array.prototype.flatmap "^1.3.2" + array.prototype.flatmap "^1.3.3" array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" - es-iterator-helpers "^1.1.0" + es-iterator-helpers "^1.2.1" estraverse "^5.3.0" hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" object.entries "^1.1.8" object.fromentries "^2.0.8" - object.values "^1.2.0" + object.values "^1.2.1" prop-types "^15.8.1" resolve "^2.0.0-next.5" semver "^6.3.1" - string.prototype.matchall "^4.0.11" + string.prototype.matchall "^4.0.12" string.prototype.repeat "^1.0.0" eslint-scope@^5.1.1: @@ -5699,7 +5833,7 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== -fast-glob@^3.0.3, fast-glob@^3.2.9, fast-glob@^3.3.1, fast-glob@^3.3.2: +fast-glob@^3.0.3, fast-glob@^3.2.9, fast-glob@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -5721,6 +5855,17 @@ fast-glob@^3.2.7: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -5737,9 +5882,9 @@ fastest-levenshtein@^1.0.16: integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + version "1.18.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.18.0.tgz#d631d7e25faffea81887fe5ea8c9010e1b36fee0" + integrity sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw== dependencies: reusify "^1.0.4" @@ -5926,15 +6071,17 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" - integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== +function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" functions-have-names@^1.2.3: version "1.2.3" @@ -5963,22 +6110,35 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044" + integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA== dependencies: + call-bind-apply-helpers "^1.0.1" + es-define-property "^1.0.1" es-errors "^1.3.0" + es-object-atoms "^1.0.0" function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" + get-proto "^1.0.0" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-proto@^1.0.0, get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^5.0.0, get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -5991,14 +6151,14 @@ get-stream@^6.0.0, get-stream@^6.0.1: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -get-symbol-description@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" - integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== +get-symbol-description@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== dependencies: - call-bind "^1.0.5" + call-bound "^1.0.3" es-errors "^1.3.0" - get-intrinsic "^1.2.4" + get-intrinsic "^1.2.6" get-tsconfig@^4.7.5: version "4.8.1" @@ -6104,7 +6264,7 @@ globals@^13.19.0, globals@^13.20.0: dependencies: type-fest "^0.20.2" -globalthis@^1.0.3, globalthis@^1.0.4: +globalthis@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== @@ -6164,12 +6324,10 @@ gonzales-pe@^4.3.0: dependencies: minimist "^1.2.5" -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" @@ -6186,10 +6344,10 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== -has-bigints@^1.0.1, has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== +has-bigints@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== has-flag@^3.0.0: version "3.0.0" @@ -6208,31 +6366,33 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1, has-proto@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== +has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: +has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: has-symbols "^1.0.3" -hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: +hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" -hoist-non-react-statics@3, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@3, hoist-non-react-statics@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -6393,14 +6553,14 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -internal-slot@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" - integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== dependencies: es-errors "^1.3.0" - hasown "^2.0.0" - side-channel "^1.0.4" + hasown "^2.0.2" + side-channel "^1.1.0" internmap@^1.0.0: version "1.0.1" @@ -6422,6 +6582,16 @@ intl-messageformat-parser@^5.3.7: dependencies: "@formatjs/intl-numberformat" "^5.5.2" +intl-messageformat@10.7.14: + version "10.7.14" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.14.tgz#ddcbbfdb1682afe56da094f21a4ac74fc3c91552" + integrity sha512-mMGnE4E1otdEutV5vLUdCxRJygHB5ozUBxsPB5qhitewssrS/qGruq9bmvIRkkGsNeK5ZWLfYRld18UHGTIifQ== + dependencies: + "@formatjs/ecma402-abstract" "2.3.2" + "@formatjs/fast-memoize" "2.2.6" + "@formatjs/icu-messageformat-parser" "2.11.0" + tslib "2" + intl-messageformat@10.7.7: version "10.7.7" resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.7.tgz#42085e1664729d02240a03346e31a2540b1112a0" @@ -6437,13 +6607,14 @@ ipaddr.js@^2.0.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== -is-array-buffer@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" - integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== +is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" is-arrayish@^0.2.1: version "0.2.1" @@ -6456,26 +6627,29 @@ is-arrayish@^0.3.1: integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== is-async-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" - integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.0.tgz#1d1080612c493608e93168fc4458c245074c06a6" + integrity sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.3" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== dependencies: - has-bigints "^1.0.1" + has-bigints "^1.0.2" -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== +is-boolean-object@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.1.tgz#c20d0c654be05da4fbc23c562635c019e93daf89" + integrity sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bound "^1.0.2" + has-tostringtag "^1.0.2" is-buffer@~1.1.6: version "1.1.6" @@ -6483,13 +6657,13 @@ is-buffer@~1.1.6: integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== is-bun-module@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-1.2.1.tgz#495e706f42e29f086fd5fe1ac3c51f106062b9fc" - integrity sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q== + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-1.3.0.tgz#ea4d24fdebfcecc98e81bcbcb506827fee288760" + integrity sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA== dependencies: semver "^7.6.3" -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: +is-callable@^1.1.3, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== @@ -6501,10 +6675,10 @@ is-ci@^3.0.1: dependencies: ci-info "^3.2.0" -is-core-module@^2.13.0, is-core-module@^2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== +is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: hasown "^2.0.2" @@ -6515,19 +6689,22 @@ is-core-module@^2.5.0: dependencies: hasown "^2.0.0" -is-data-view@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" - integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== +is-data-view@^1.0.1, is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" is-typed-array "^1.1.13" -is-date-object@^1.0.1, is-date-object@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.2" + has-tostringtag "^1.0.2" is-docker@^3.0.0: version "3.0.0" @@ -6539,12 +6716,12 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-finalizationregistry@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" - integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== dependencies: - call-bind "^1.0.2" + call-bound "^1.0.3" is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -6562,11 +6739,14 @@ is-generator-fn@^2.0.0: integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== is-generator-function@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca" + integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.3" + get-proto "^1.0.0" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" @@ -6598,17 +6778,13 @@ is-module@^1.0.0: resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== -is-negative-zero@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" - integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== - -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.3" + has-tostringtag "^1.0.2" is-number@^7.0.0: version "7.0.0" @@ -6652,25 +6828,27 @@ is-reference@1.2.1: dependencies: "@types/estree" "*" -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== -is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" - integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== +is-shared-array-buffer@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" is-stream@^2.0.0: version "2.0.1" @@ -6682,26 +6860,29 @@ is-stream@^3.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== +is-string@^1.0.7, is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.3" + has-tostringtag "^1.0.2" -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== dependencies: - has-symbols "^1.0.2" + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" -is-typed-array@^1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== +is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== dependencies: - which-typed-array "^1.1.14" + which-typed-array "^1.1.16" is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" @@ -6718,20 +6899,20 @@ is-weakmap@^2.0.2: resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== +is-weakref@^1.0.2, is-weakref@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.0.tgz#47e3472ae95a63fa9cf25660bcf0c181c39770ef" + integrity sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q== dependencies: - call-bind "^1.0.2" + call-bound "^1.0.2" is-weakset@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" - integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" isarray@^2.0.5: version "2.0.5" @@ -6739,9 +6920,9 @@ isarray@^2.0.5: integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== isbot@^5.1.16: - version "5.1.17" - resolved "https://registry.yarnpkg.com/isbot/-/isbot-5.1.17.tgz#ad7da5690a61bbb19056a069975c9a73182682a0" - integrity sha512-/wch8pRKZE+aoVhRX/hYPY1C7dMCeeMyhkQLNLNlYAbGQn9bkvMB8fOUXNnk5I0m4vDYbBJ9ciVtkr9zfBJ7qA== + version "5.1.21" + resolved "https://registry.yarnpkg.com/isbot/-/isbot-5.1.21.tgz#4e27526a71c8b9c243b1c7a6445ad4267fa83728" + integrity sha512-0q3naRVpENL0ReKHeNcwn/G7BDynp0DqZUckKyFtM9+hmpnPqgm8+8wbjiVZ0XNhq1wPQV28/Pb8Snh5adeUHA== isexe@^2.0.0: version "2.0.0" @@ -6806,23 +6987,24 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -iterator.prototype@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.3.tgz#016c2abe0be3bbdb8319852884f60908ac62bf9c" - integrity sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ== +iterator.prototype@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39" + integrity sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g== dependencies: - define-properties "^1.2.1" - get-intrinsic "^1.2.1" - has-symbols "^1.0.3" - reflect.getprototypeof "^1.0.4" - set-function-name "^2.0.1" + define-data-property "^1.1.4" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + get-proto "^1.0.0" + has-symbols "^1.1.0" + set-function-name "^2.0.2" -jackspeak@2.1.1, jackspeak@^2.3.5: - version "2.1.1" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.1.1.tgz#2a42db4cfbb7e55433c28b6f75d8b796af9669cd" - integrity sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw== +jackspeak@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== dependencies: - cliui "^8.0.1" + "@isaacs/cliui" "^8.0.2" optionalDependencies: "@pkgjs/parseargs" "^0.11.0" @@ -7328,7 +7510,7 @@ jsonify@^0.0.1: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== -jsonwebtoken@^9.0.0: +jsonwebtoken@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== @@ -7529,11 +7711,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== - lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -7734,15 +7911,20 @@ map-obj@^4.0.0, map-obj@^4.1.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + mathml-tag-names@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -maxmind@^4.3.6: - version "4.3.22" - resolved "https://registry.yarnpkg.com/maxmind/-/maxmind-4.3.22.tgz#8168a2d890d88626613b97eeecbb13fabe0074c4" - integrity sha512-dfLO11mE77ELTEIXNezfW0eslodsFLsZ1lQkLauP+5Zsg1m7kCGtljqRyVOd9E5Ne2RJgvY6UU09qvnVocOZvA== +maxmind@^4.3.24: + version "4.3.24" + resolved "https://registry.yarnpkg.com/maxmind/-/maxmind-4.3.24.tgz#c67a4278777210c857434fa8e82bdd6774e5e661" + integrity sha512-dexrLcjfS2xDGOvdV8XcfQYmyQVpGidMwEG2ld19lXlsB+i+lXRWPzQi81HfwRXR4hxzFr5gT0oAIFyqAAb/Ww== dependencies: mmdb-lib "2.1.1" tiny-lru "11.2.11" @@ -7771,11 +7953,16 @@ mdn-data@2.0.30: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== -"memoize-one@>=3.1.1 <6", memoize-one@^5.1.1: +"memoize-one@>=3.1.1 <6": version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" @@ -7834,7 +8021,7 @@ micromatch@4.0.5, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -micromatch@^4.0.4: +micromatch@^4.0.4, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -7956,30 +8143,16 @@ ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoclone@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" - integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== - nanoid@^3.3.6, nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next-basics@^0.39.0: - version "0.39.0" - resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.39.0.tgz#1ec448a1c12966a82067445bfb9319b7e883dd6a" - integrity sha512-5HWf3u7jgx5n4auIkArFP5+EVdyz7kSvxs86o2V4y8/t3J4scdIHgI8BBE6UhzB17WMbMgVql44IfcJH1CQc/w== - dependencies: - bcryptjs "^2.4.3" - jsonwebtoken "^9.0.0" - pure-rand "^6.0.2" - next@15.0.4: version "15.0.4" resolved "https://registry.yarnpkg.com/next/-/next-15.0.4.tgz#7ddad7299204f16c132d7e524cf903f1a513588e" @@ -8121,7 +8294,7 @@ object-assign@^4, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.13.1, object-inspect@^1.13.3: +object-inspect@^1.13.3: version "1.13.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== @@ -8131,14 +8304,16 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.4, object.assign@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== +object.assign@^4.1.4, object.assign@^4.1.7: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== dependencies: - call-bind "^1.0.5" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" - has-symbols "^1.0.3" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" object-keys "^1.1.1" object.entries@^1.1.8: @@ -8169,12 +8344,13 @@ object.groupby@^1.0.3: define-properties "^1.2.1" es-abstract "^1.23.2" -object.values@^1.1.6, object.values@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" - integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== +object.values@^1.1.6, object.values@^1.2.0, object.values@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" es-object-atoms "^1.0.0" @@ -8216,6 +8392,15 @@ ospath@^1.2.2: resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" integrity sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA== +own-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== + dependencies: + get-intrinsic "^1.2.6" + object-keys "^1.1.1" + safe-push-apply "^1.0.0" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -9030,12 +9215,12 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" -prisma@5.22.0: - version "5.22.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.22.0.tgz#1f6717ff487cdef5f5799cc1010459920e2e6197" - integrity sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A== +prisma@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-6.1.0.tgz#738f657fdd5ab8e6775f385db81bf7e61c70fbaf" + integrity sha512-aFI3Yi+ApUxkwCJJwyQSwpyzUX7YX3ihzuHNHOyv4GJg3X5tQsmRaJEnZ+ZyfHpMtnyahhmXVfbTZ+lS8ZtfKw== dependencies: - "@prisma/engines" "5.22.0" + "@prisma/engines" "6.1.0" optionalDependencies: fsevents "2.3.3" @@ -9057,7 +9242,7 @@ prompts@2.4.2, prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -9066,11 +9251,6 @@ prop-types@^15.7.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -property-expr@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" - integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== - proxy-from-env@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" @@ -9089,11 +9269,16 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -pure-rand@^6.0.0, pure-rand@^6.0.2: +pure-rand@^6.0.0: version "6.0.4" resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.4.tgz#50b737f6a925468679bff00ad20eade53f37d5c7" integrity sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA== +pure-rand@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + qs@6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" @@ -9116,7 +9301,7 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -raf-schd@^4.0.2: +raf-schd@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== @@ -9128,10 +9313,10 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -react-basics@^0.125.0: - version "0.125.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.125.0.tgz#6baf3fea503fb4475f51877efa05d1a734b232c6" - integrity sha512-8swjTaKfenwb+NunwzQo16V+dCA/38Kd+PSYWpBFyNmlFzs3Ax2ZgnysxDhW9IgfFr4wR6/0gzD3S31WzXq6Kw== +react-basics@^0.126.0: + version "0.126.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.126.0.tgz#44e7f5e5ab9d411e91e697dd39c6cb53b6222ae0" + integrity sha512-TQtNZMeH5FtJjYxSN72rBmZWlIcs9jK3oVSCUUxfZq9LnFdoFSagTLCrihs3YCnX8vZEJXaJHQsp7lKEfyH5sw== dependencies: "@react-spring/web" "^9.7.3" classnames "^2.3.1" @@ -9139,19 +9324,6 @@ react-basics@^0.125.0: react-hook-form "^7.34.2" react-window "^1.8.6" -react-beautiful-dnd@^13.1.0: - version "13.1.1" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" - integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== - dependencies: - "@babel/runtime" "^7.9.2" - css-box-model "^1.2.0" - memoize-one "^5.1.1" - raf-schd "^4.0.2" - react-redux "^7.2.0" - redux "^4.0.4" - use-memo-one "^1.1.1" - react-dom@^19.0.0: version "19.0.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57" @@ -9171,6 +9343,20 @@ react-hook-form@^7.34.2: resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.47.0.tgz#a42f07266bd297ddf1f914f08f4b5f9783262f31" integrity sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg== +react-intl@*: + version "7.1.5" + resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-7.1.5.tgz#086c1b7cfb00ab7c9f62241162aca86520a5dc4c" + integrity sha512-cVvsVdaOnZ85XBXU0Lc2PVGNhGlzl4UBV+aWAGe/zrV5Xr+CEW7izUsAp/fIuwvCsJl9R+aokppm+P7cdhnpUA== + dependencies: + "@formatjs/ecma402-abstract" "2.3.2" + "@formatjs/icu-messageformat-parser" "2.11.0" + "@formatjs/intl" "3.1.3" + "@types/hoist-non-react-statics" "3" + "@types/react" "16 || 17 || 18 || 19" + hoist-non-react-statics "3" + intl-messageformat "10.7.14" + tslib "2" + react-intl@^6.5.5: version "6.8.9" resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-6.8.9.tgz#ef36b2a19a0eb97afbeaeab9679273fcbf2ea261" @@ -9192,27 +9378,18 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - react-is@^18.0.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-redux@^7.2.0: - version "7.2.9" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" - integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== +react-redux@^9.1.2: + version "9.2.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.2.0.tgz#96c3ab23fb9a3af2cb4654be4b51c989e32366f5" + integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g== dependencies: - "@babel/runtime" "^7.15.4" - "@types/react-redux" "^7.1.20" - hoist-non-react-statics "^3.3.2" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^17.0.2" + "@types/use-sync-external-store" "^0.0.6" + use-sync-external-store "^1.4.0" react-simple-maps@^2.3.0: version "2.3.0" @@ -9232,9 +9409,9 @@ react-use-measure@^2.0.4: debounce "^1.2.1" react-window@^1.8.6: - version "1.8.10" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" - integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg== + version "1.8.11" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.11.tgz#a857b48fa85bd77042d59cc460964ff2e0648525" + integrity sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ== dependencies: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" @@ -9334,25 +9511,24 @@ redis@^4.5.1: "@redis/search" "1.1.5" "@redis/time-series" "1.0.5" -redux@^4.0.0, redux@^4.0.4: - version "4.2.1" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" - integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== - dependencies: - "@babel/runtime" "^7.9.2" +redux@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" + integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== -reflect.getprototypeof@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" - integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" define-properties "^1.2.1" - es-abstract "^1.23.1" + es-abstract "^1.23.9" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - globalthis "^1.0.3" - which-builtin-type "^1.1.3" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.7" + get-proto "^1.0.1" + which-builtin-type "^1.2.1" regenerate-unicode-properties@^10.1.0: version "10.1.1" @@ -9378,14 +9554,16 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regexp.prototype.flags@^1.5.2, regexp.prototype.flags@^1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" - integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== +regexp.prototype.flags@^1.5.3: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" define-properties "^1.2.1" es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" set-function-name "^2.0.2" regexpu-core@^5.3.1: @@ -9456,7 +9634,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.4: +resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -9465,6 +9643,15 @@ resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.1, resolve@^1.22.4: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.5: version "2.0.0-next.5" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" @@ -9617,14 +9804,15 @@ rxjs@^7.5.1: dependencies: tslib "^2.1.0" -safe-array-concat@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" - integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== +safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" - has-symbols "^1.0.3" + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" isarray "^2.0.5" safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2: @@ -9637,14 +9825,22 @@ safe-identifier@^0.4.2: resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb" integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w== -safe-regex-test@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" - integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== +safe-push-apply@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== dependencies: - call-bind "^1.0.6" es-errors "^1.3.0" - is-regex "^1.1.4" + isarray "^2.0.5" + +safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" @@ -9690,6 +9886,13 @@ semver@^7.3.4, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semve resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +serialize-error@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-12.0.0.tgz#aed3d5abff192c855707513929bf8bf48d712194" + integrity sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw== + dependencies: + type-fest "^4.31.0" + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -9697,7 +9900,7 @@ serialize-javascript@^4.0.0: dependencies: randombytes "^2.1.0" -set-function-length@^1.2.1: +set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== @@ -9709,7 +9912,7 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" -set-function-name@^2.0.1, set-function-name@^2.0.2: +set-function-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== @@ -9719,6 +9922,15 @@ set-function-name@^2.0.1, set-function-name@^2.0.2: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" +set-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== + dependencies: + dunder-proto "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + sharp@^0.33.5: version "0.33.5" resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e" @@ -9777,15 +9989,45 @@ shell-quote@^1.6.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -side-channel@^1.0.4, side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: - call-bind "^1.0.7" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6, side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" @@ -9937,6 +10179,11 @@ sshpk@^1.18.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +stable-hash@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/stable-hash/-/stable-hash-0.0.4.tgz#55ae7dadc13e4b3faed13601587cec41859b42f7" + integrity sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g== + stable@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" @@ -9972,6 +10219,15 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -9981,7 +10237,7 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.0, string-width@^5.0.1: +string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== @@ -9999,23 +10255,24 @@ string.prototype.includes@^2.0.1: define-properties "^1.2.1" es-abstract "^1.23.3" -string.prototype.matchall@^4.0.11: - version "4.0.11" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" - integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== +string.prototype.matchall@^4.0.12: + version "4.0.12" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0" + integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" - es-abstract "^1.23.2" + es-abstract "^1.23.6" es-errors "^1.3.0" es-object-atoms "^1.0.0" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-symbols "^1.0.3" - internal-slot "^1.0.7" - regexp.prototype.flags "^1.5.2" + get-intrinsic "^1.2.6" + gopd "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + regexp.prototype.flags "^1.5.3" set-function-name "^2.0.2" - side-channel "^1.0.6" + side-channel "^1.1.0" string.prototype.padend@^3.0.0: version "3.1.5" @@ -10034,22 +10291,26 @@ string.prototype.repeat@^1.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" -string.prototype.trim@^1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" - integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" define-properties "^1.2.1" - es-abstract "^1.23.0" + es-abstract "^1.23.5" es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" -string.prototype.trimend@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" - integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== +string.prototype.trimend@^1.0.8, string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.2" define-properties "^1.2.1" es-object-atoms "^1.0.0" @@ -10062,6 +10323,13 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -10401,11 +10669,6 @@ topojson-client@^3.1.0: dependencies: commander "2" -toposort@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" - integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== - tough-cookie@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af" @@ -10428,11 +10691,16 @@ trim-newlines@^4.0.2: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.1.1.tgz#28c88deb50ed10c7ba6dc2474421904a00139125" integrity sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ== -ts-api-utils@^1.0.1, ts-api-utils@^1.3.0: +ts-api-utils@^1.0.1: version "1.4.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.0.tgz#709c6f2076e511a81557f3d07a0cbd566ae8195c" integrity sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ== +ts-api-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.0.tgz#b9d7d5f7ec9f736f4d0f09758b8607979044a900" + integrity sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ== + ts-jest@^29.1.2: version "29.2.5" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.5.tgz#591a3c108e1f5ebd013d3152142cb5472b399d63" @@ -10553,49 +10821,55 @@ type-fest@^1.0.1, type-fest@^1.0.2, type-fest@^1.2.1, type-fest@^1.2.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -typed-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" - integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== +type-fest@^4.31.0: + version "4.31.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.31.0.tgz#a3de630c96eb77c281b6ba2affa5dae5fb3c326c" + integrity sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ== + +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" es-errors "^1.3.0" - is-typed-array "^1.1.13" + is-typed-array "^1.1.14" -typed-array-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" - integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== +typed-array-byte-length@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" -typed-array-byte-offset@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" - integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== +typed-array-byte-offset@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== dependencies: available-typed-arrays "^1.0.7" - call-bind "^1.0.7" + call-bind "^1.0.8" for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" -typed-array-length@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" - integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== dependencies: call-bind "^1.0.7" for-each "^0.3.3" gopd "^1.0.1" - has-proto "^1.0.3" is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" typedarray-to-buffer@^3.1.5: version "3.1.5" @@ -10610,25 +10884,30 @@ typescript@^4.0, typescript@^4.5: integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== typescript@^5.5.3: - version "5.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" - integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== + version "5.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" + integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== -unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== +unbox-primitive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== dependencies: - call-bind "^1.0.2" + call-bound "^1.0.3" has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" -undici-types@~6.19.2, undici-types@~6.19.8: +undici-types@~6.19.8: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -10682,15 +10961,15 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-memo-one@^1.1.1: +use-memo-one@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== -use-sync-external-store@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" - integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== +use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc" + integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw== util-deprecate@^1.0.2: version "1.0.2" @@ -10766,34 +11045,35 @@ web-streams-polyfill@^3.0.3: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" -which-builtin-type@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" - integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== dependencies: + call-bound "^1.0.2" function.prototype.name "^1.1.6" has-tostringtag "^1.0.2" is-async-function "^2.0.0" - is-date-object "^1.0.5" - is-finalizationregistry "^1.0.2" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" is-generator-function "^1.0.10" - is-regex "^1.1.4" + is-regex "^1.2.1" is-weakref "^1.0.2" isarray "^2.0.5" - which-boxed-primitive "^1.0.2" + which-boxed-primitive "^1.1.0" which-collection "^1.0.2" - which-typed-array "^1.1.15" + which-typed-array "^1.1.16" which-collection@^1.0.2: version "1.0.2" @@ -10805,15 +11085,16 @@ which-collection@^1.0.2: is-weakmap "^2.0.2" is-weakset "^2.0.3" -which-typed-array@^1.1.14, which-typed-array@^1.1.15: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== +which-typed-array@^1.1.16, which-typed-array@^1.1.18: + version "1.1.18" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.18.tgz#df2389ebf3fbb246a71390e90730a9edb6ce17ad" + integrity sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA== dependencies: available-typed-arrays "^1.0.7" - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" for-each "^0.3.3" - gopd "^1.0.1" + gopd "^1.2.0" has-tostringtag "^1.0.2" which@^1.2.9, which@^1.3.1: @@ -10835,6 +11116,15 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -10979,22 +11269,14 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yup@^0.32.11: - version "0.32.11" - resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" - integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== - dependencies: - "@babel/runtime" "^7.15.4" - "@types/lodash" "^4.14.175" - lodash "^4.17.21" - lodash-es "^4.17.21" - nanoclone "^0.2.1" - property-expr "^2.0.4" - toposort "^2.0.2" +zod@^3.24.1: + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== zustand@^4.5.5: - version "4.5.5" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1" - integrity sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q== + version "4.5.6" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.6.tgz#6857d52af44874a79fb3408c9473f78367255c96" + integrity sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ== dependencies: - use-sync-external-store "1.2.2" + use-sync-external-store "^1.2.2"