diff --git a/next.config.js b/next.config.js index cc3cde7c..2ef1c05e 100644 --- a/next.config.js +++ b/next.config.js @@ -6,7 +6,7 @@ const pkg = require('./package.json'); const contentSecurityPolicy = ` default-src 'self'; img-src *; - script-src 'self' 'unsafe-eval'; + script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' api.umami.is; frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS}; @@ -74,16 +74,20 @@ if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN }); } +const basePath = process.env.BASE_PATH; + +/** @type {import('next').NextConfig} */ const config = { env: { - cloudMode: process.env.CLOUD_MODE, + basePath: basePath || '', + cloudMode: !!process.env.CLOUD_MODE, cloudUrl: process.env.CLOUD_URL, configUrl: '/config', currentVersion: pkg.version, defaultLocale: process.env.DEFAULT_LOCALE, isProduction: process.env.NODE_ENV === 'production', }, - basePath: process.env.BASE_PATH, + basePath, output: 'standalone', eslint: { ignoreDuringBuilds: true, @@ -92,11 +96,23 @@ const config = { ignoreBuildErrors: true, }, webpack(config) { - config.module.rules.push({ - test: /\.svg$/, - issuer: /\.{js|jsx|ts|tsx}$/, - use: ['@svgr/webpack'], - }); + 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'); diff --git a/package.json b/package.json index 79960eb2..5b005f66 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@fontsource/inter": "^4.5.15", "@prisma/client": "5.3.1", "@tanstack/react-query": "^4.33.0", - "@umami/prisma-client": "^0.2.0", + "@umami/prisma-client": "^0.3.0", "@umami/redis-client": "^0.15.0", "chalk": "^4.1.1", "chart.js": "^4.2.1", @@ -91,7 +91,7 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "13.5.2", + "next": "13.5.3", "next-basics": "^0.36.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", @@ -100,7 +100,7 @@ "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", - "react-intl": "^5.24.7", + "react-intl": "^6.4.7", "react-simple-maps": "^2.3.0", "react-spring": "^9.4.4", "react-use-measure": "^2.0.4", @@ -123,12 +123,12 @@ "@rollup/plugin-node-resolve": "^15.2.0", "@rollup/plugin-replace": "^5.0.2", "@svgr/rollup": "^8.1.0", - "@svgr/webpack": "^6.2.1", + "@svgr/webpack": "^8.1.0", "@types/node": "^18.11.9", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.8", - "@typescript-eslint/eslint-plugin": "^5.50.0", - "@typescript-eslint/parser": "^5.50.0", + "@typescript-eslint/eslint-plugin": "^6.7.3", + "@typescript-eslint/parser": "^6.7.3", "cross-env": "^7.0.3", "esbuild": "^0.17.17", "eslint": "^8.33.0", @@ -138,8 +138,8 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.0.0", "extract-react-intl-messages": "^4.1.1", - "husky": "^7.0.0", - "lint-staged": "^11.0.0", + "husky": "^8.0.3", + "lint-staged": "^14.0.1", "postcss": "^8.4.21", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^15.1.0", diff --git a/src/app/(app)/NavBar.js b/src/app/(app)/NavBar.js new file mode 100644 index 00000000..211adf5f --- /dev/null +++ b/src/app/(app)/NavBar.js @@ -0,0 +1,58 @@ +'use client'; +import { Icon, Text } from 'react-basics'; +import Link from 'next/link'; +import classNames from 'classnames'; +import Icons from 'components/icons'; +import ThemeButton from 'components/input/ThemeButton'; +import LanguageButton from 'components/input/LanguageButton'; +import ProfileButton from 'components/input/ProfileButton'; +import useMessages from 'components/hooks/useMessages'; +import HamburgerButton from 'components/common/HamburgerButton'; +import { usePathname } from 'next/navigation'; +import styles from './NavBar.module.css'; + +export function NavBar() { + const pathname = usePathname(); + const { formatMessage, labels } = useMessages(); + + const links = [ + { label: formatMessage(labels.dashboard), url: '/dashboard' }, + { label: formatMessage(labels.websites), url: '/websites' }, + { label: formatMessage(labels.reports), url: '/reports' }, + { label: formatMessage(labels.settings), url: '/settings' }, + ].filter(n => n); + + return ( +
+
+ + + + umami +
+
+ {links.map(({ url, label }) => { + return ( + + {label} + + ); + })} +
+
+ + + +
+
+ +
+
+ ); +} + +export default NavBar; diff --git a/src/components/layout/NavBar.module.css b/src/app/(app)/NavBar.module.css similarity index 75% rename from src/components/layout/NavBar.module.css rename to src/app/(app)/NavBar.module.css index dd5085a0..fd022eca 100644 --- a/src/components/layout/NavBar.module.css +++ b/src/app/(app)/NavBar.module.css @@ -1,7 +1,7 @@ .navbar { + display: grid; + grid-template-columns: max-content 1fr 1fr; position: relative; - display: flex; - flex-direction: row; align-items: center; height: 60px; background: var(--base75); @@ -9,17 +9,6 @@ padding: 0 20px; } -.left, -.right { - display: flex; - flex-direction: row; - align-items: center; -} - -.right { - justify-content: flex-end; -} - .logo { display: flex; flex-direction: row; @@ -35,29 +24,24 @@ flex-direction: row; gap: 30px; padding: 0 40px; - flex: 1; font-weight: 700; + max-height: 60px; } -.links a { - display: flex; - align-items: center; - gap: 10px; - line-height: 60px; +.links a, +.links a:active, +.links a:visited { color: var(--font-color200); + line-height: 60px; border-bottom: 2px solid transparent; } -.links span { - white-space: nowrap; -} - .links a:hover { color: var(--font-color100); border-bottom: 2px solid var(--primary400); } -.links .selected { +.links a.selected { color: var(--font-color100); border-bottom: 2px solid var(--primary400); } @@ -68,7 +52,6 @@ flex-direction: row; align-items: center; justify-content: flex-end; - min-width: 0; } .mobile { @@ -76,6 +59,10 @@ } @media only screen and (max-width: 768px) { + .navbar { + grid-template-columns: repeat(2, 1fr); + } + .links, .actions { display: none; diff --git a/src/app/(app)/Shell.tsx b/src/app/(app)/Shell.tsx new file mode 100644 index 00000000..980abb62 --- /dev/null +++ b/src/app/(app)/Shell.tsx @@ -0,0 +1,27 @@ +'use client'; +import Script from 'next/script'; +import { usePathname } from 'next/navigation'; +import UpdateNotice from 'components/common/UpdateNotice'; +import { useRequireLogin, useConfig } from 'components/hooks'; + +export function Shell({ children }) { + const { user } = useRequireLogin(); + const config = useConfig(); + const pathname = usePathname(); + + if (!user || !config) { + return null; + } + + return ( + <> + {children} + + {process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && ( + `; diff --git a/src/components/pages/settings/websites/WebsiteData.js b/src/app/(app)/settings/websites/[id]/WebsiteData.js similarity index 89% rename from src/components/pages/settings/websites/WebsiteData.js rename to src/app/(app)/settings/websites/[id]/WebsiteData.js index 08d6702e..07dc9257 100644 --- a/src/components/pages/settings/websites/WebsiteData.js +++ b/src/app/(app)/settings/websites/[id]/WebsiteData.js @@ -1,6 +1,6 @@ import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics'; -import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm'; -import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm'; +import WebsiteDeleteForm from './WebsiteDeleteForm'; +import WebsiteResetForm from './WebsiteResetForm'; import useMessages from 'components/hooks/useMessages'; export function WebsiteData({ websiteId, onSave }) { diff --git a/src/components/pages/settings/websites/WebsiteDeleteForm.js b/src/app/(app)/settings/websites/[id]/WebsiteDeleteForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteDeleteForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteDeleteForm.js diff --git a/src/components/pages/settings/websites/WebsiteEditForm.js b/src/app/(app)/settings/websites/[id]/WebsiteEditForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteEditForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteEditForm.js diff --git a/src/components/pages/settings/websites/WebsiteResetForm.js b/src/app/(app)/settings/websites/[id]/WebsiteResetForm.js similarity index 100% rename from src/components/pages/settings/websites/WebsiteResetForm.js rename to src/app/(app)/settings/websites/[id]/WebsiteResetForm.js diff --git a/src/app/(app)/settings/websites/[id]/page.js b/src/app/(app)/settings/websites/[id]/page.js new file mode 100644 index 00000000..bdf3b076 --- /dev/null +++ b/src/app/(app)/settings/websites/[id]/page.js @@ -0,0 +1,15 @@ +import WebsiteSettings from '../WebsiteSettings'; + +async function getDisabled() { + return !!process.env.CLOUD_MODE; +} + +export default async function WebsiteSettingsPage({ params }) { + const disabled = await getDisabled(); + + if (!params.id || disabled) { + return null; + } + + return ; +} diff --git a/src/app/(app)/settings/websites/page.js b/src/app/(app)/settings/websites/page.js new file mode 100644 index 00000000..ade3e3ad --- /dev/null +++ b/src/app/(app)/settings/websites/page.js @@ -0,0 +1,9 @@ +import WebsitesList from 'app/(app)/settings/websites/WebsitesList'; + +export default function () { + if (process.env.cloudMode) { + return null; + } + + return ; +} diff --git a/src/components/pages/websites/WebsiteTableView.js b/src/app/(app)/websites/WebsiteTableView.js similarity index 100% rename from src/components/pages/websites/WebsiteTableView.js rename to src/app/(app)/websites/WebsiteTableView.js diff --git a/src/components/pages/websites/WebsiteTableView.module.css b/src/app/(app)/websites/WebsiteTableView.module.css similarity index 100% rename from src/components/pages/websites/WebsiteTableView.module.css rename to src/app/(app)/websites/WebsiteTableView.module.css diff --git a/src/components/pages/websites/WebsiteChart.js b/src/app/(app)/websites/[id]/WebsiteChart.js similarity index 100% rename from src/components/pages/websites/WebsiteChart.js rename to src/app/(app)/websites/[id]/WebsiteChart.js diff --git a/src/components/pages/websites/WebsiteChart.module.css b/src/app/(app)/websites/[id]/WebsiteChart.module.css similarity index 100% rename from src/components/pages/websites/WebsiteChart.module.css rename to src/app/(app)/websites/[id]/WebsiteChart.module.css diff --git a/src/components/pages/websites/WebsiteChartList.js b/src/app/(app)/websites/[id]/WebsiteChartList.js similarity index 90% rename from src/components/pages/websites/WebsiteChartList.js rename to src/app/(app)/websites/[id]/WebsiteChartList.js index 56cbe157..23764dbb 100644 --- a/src/components/pages/websites/WebsiteChartList.js +++ b/src/app/(app)/websites/[id]/WebsiteChartList.js @@ -2,9 +2,8 @@ import { Button, Text, Icon } from 'react-basics'; import { useMemo } from 'react'; import { firstBy } from 'thenby'; import Link from 'next/link'; -import WebsiteChart from 'components/pages/websites/WebsiteChart'; +import WebsiteChart from './WebsiteChart'; import useDashboard from 'store/dashboard'; -import styles from './WebsiteList.module.css'; import WebsiteHeader from './WebsiteHeader'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; import { useMessages, useLocale } from 'components/hooks'; @@ -27,7 +26,7 @@ export default function WebsiteChartList({ websites, showCharts, limit }) {
{ordered.map(({ id }, index) => { return index < limit ? ( -
+
- - - - {({ data }) => } - - - ); -} - -export default ReportsPage; diff --git a/src/components/pages/settings/profile/ProfileSettings.js b/src/components/pages/settings/profile/ProfileSettings.js deleted file mode 100644 index a217e52c..00000000 --- a/src/components/pages/settings/profile/ProfileSettings.js +++ /dev/null @@ -1,17 +0,0 @@ -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import ProfileDetails from './ProfileDetails'; -import useMessages from 'components/hooks/useMessages'; - -export function ProfileSettings() { - const { formatMessage, labels } = useMessages(); - - return ( - - - - - ); -} - -export default ProfileSettings; diff --git a/src/components/pages/settings/teams/TeamsList.js b/src/components/pages/settings/teams/TeamsList.js deleted file mode 100644 index 76a87b0c..00000000 --- a/src/components/pages/settings/teams/TeamsList.js +++ /dev/null @@ -1,118 +0,0 @@ -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import Icons from 'components/icons'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import TeamAddForm from 'components/pages/settings/teams/TeamAddForm'; -import TeamsTable from 'components/pages/settings/teams/TeamsTable'; -import useApi from 'components/hooks/useApi'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { useState } from 'react'; -import { Button, Flexbox, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import TeamJoinForm from './TeamJoinForm'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function TeamsList() { - const { user } = useUser(); - const { formatMessage, labels, messages } = useMessages(); - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - const [update, setUpdate] = useState(0); - - const { get, useQuery } = useApi(); - const { data, isLoading, error } = useQuery(['teams', update, filter, page, pageSize], () => { - return get(`/teams`, { - filter, - page, - pageSize, - }); - }); - - const hasData = data && data?.data.length !== 0; - const isFiltered = filter; - - const { showToast } = useToasts(); - - const handleSave = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const handleJoin = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const handleDelete = () => { - setUpdate(state => state + 1); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const joinButton = ( - - - - {close => } - - - ); - - const createButton = ( - <> - {user.role !== ROLES.viewOnly && ( - - - - {close => } - - - )} - - ); - - return ( - - - {(hasData || isFiltered) && ( - - {joinButton} - {createButton} - - )} - - - {(hasData || isFiltered) && ( - - )} - - {!hasData && !isFiltered && ( - - - {joinButton} - {createButton} - - - )} - - ); -} - -export default TeamsList; diff --git a/src/components/pages/settings/teams/TeamsTable.js b/src/components/pages/settings/teams/TeamsTable.js deleted file mode 100644 index e1710783..00000000 --- a/src/components/pages/settings/teams/TeamsTable.js +++ /dev/null @@ -1,111 +0,0 @@ -import SettingsTable from 'components/common/SettingsTable'; -import useLocale from 'components/hooks/useLocale'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import Link from 'next/link'; -import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; -import TeamDeleteForm from './TeamDeleteForm'; -import TeamLeaveForm from './TeamLeaveForm'; - -export function TeamsTable({ - data = { data: [] }, - onDelete, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - const { dir } = useLocale(); - - const columns = [ - { name: 'name', label: formatMessage(labels.name) }, - { name: 'owner', label: formatMessage(labels.owner) }, - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'owner') { - return row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username; - } - return data[key]; - }; - - return ( - - {row => { - const { id, teamUser } = row; - const owner = teamUser.find(({ role }) => role === ROLES.teamOwner); - const showDelete = user.id === owner?.userId; - - return ( - <> - - - - {showDelete && ( - - - - {close => ( - - )} - - - )} - {!showDelete && ( - - - - {close => ( - - )} - - - )} - - ); - }} - - ); -} - -export default TeamsTable; diff --git a/src/components/pages/settings/users/UsersList.js b/src/components/pages/settings/users/UsersList.js deleted file mode 100644 index 0bc8612e..00000000 --- a/src/components/pages/settings/users/UsersList.js +++ /dev/null @@ -1,68 +0,0 @@ -import { useToasts } from 'react-basics'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import UsersTable from './UsersTable'; -import UserAddButton from './UserAddButton'; -import useApi from 'components/hooks/useApi'; -import useUser from 'components/hooks/useUser'; -import useMessages from 'components/hooks/useMessages'; -import useApiFilter from 'components/hooks/useApiFilter'; - -export function UsersList() { - const { formatMessage, labels, messages } = useMessages(); - const { user } = useUser(); - const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } = - useApiFilter(); - - const { get, useQuery } = useApi(); - const { data, isLoading, error, refetch } = useQuery( - ['user', filter, page, pageSize], - () => - get(`/users`, { - filter, - page, - pageSize, - }), - { - enabled: !!user, - }, - ); - const { showToast } = useToasts(); - const hasData = data && data.length !== 0; - - const handleSave = () => { - refetch().then(() => showToast({ message: formatMessage(messages.saved), variant: 'success' })); - }; - - const handleDelete = () => { - refetch().then(() => - showToast({ message: formatMessage(messages.userDeleted), variant: 'success' }), - ); - }; - - return ( - - - - - {(hasData || filter) && ( - - )} - {!hasData && !filter && ( - - - - )} - - ); -} - -export default UsersList; diff --git a/src/components/pages/settings/users/UsersTable.js b/src/components/pages/settings/users/UsersTable.js deleted file mode 100644 index 1a93710d..00000000 --- a/src/components/pages/settings/users/UsersTable.js +++ /dev/null @@ -1,93 +0,0 @@ -import { Button, Text, Icon, Icons, ModalTrigger, Modal } from 'react-basics'; -import { formatDistance } from 'date-fns'; -import Link from 'next/link'; -import useUser from 'components/hooks/useUser'; -import UserDeleteForm from './UserDeleteForm'; -import { ROLES } from 'lib/constants'; -import useMessages from 'components/hooks/useMessages'; -import SettingsTable from 'components/common/SettingsTable'; -import useLocale from 'components/hooks/useLocale'; - -export function UsersTable({ - data = { data: [] }, - onDelete, - filterValue, - onFilterChange, - onPageChange, - onPageSizeChange, -}) { - const { formatMessage, labels } = useMessages(); - const { user } = useUser(); - const { dateLocale } = useLocale(); - - const columns = [ - { name: 'username', label: formatMessage(labels.username) }, - { name: 'role', label: formatMessage(labels.role) }, - { name: 'created', label: formatMessage(labels.created) }, - { name: 'action', label: ' ' }, - ]; - - const cellRender = (row, data, key) => { - if (key === 'created') { - return formatDistance(new Date(row.createdAt), new Date(), { - addSuffix: true, - locale: dateLocale, - }); - } - if (key === 'role') { - return formatMessage( - labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown, - ); - } - return data[key]; - }; - - return ( - - {row => { - return ( - <> - - - - - - - {close => ( - - )} - - - - ); - }} - - ); -} - -export default UsersTable; diff --git a/src/components/pages/settings/websites/WebsitesList.js b/src/components/pages/settings/websites/WebsitesList.js deleted file mode 100644 index 0dd3aa77..00000000 --- a/src/components/pages/settings/websites/WebsitesList.js +++ /dev/null @@ -1,71 +0,0 @@ -import PageHeader from 'components/layout/PageHeader'; -import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; -import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; -import useMessages from 'components/hooks/useMessages'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; -import useApi from 'components/hooks/useApi'; -import DataTable from 'components/common/DataTable'; -import useFilterQuery from 'components/hooks/useFilterQuery'; - -export function WebsitesList({ - showTeam, - showEditButton = true, - showHeader = true, - includeTeams, - onlyTeams, -}) { - const { formatMessage, labels, messages } = useMessages(); - const { user } = useUser(); - const { get } = useApi(); - const filterQuery = useFilterQuery( - ['websites', { includeTeams, onlyTeams }], - params => { - return get(`/users/${user?.id}/websites`, { - includeTeams, - onlyTeams, - ...params, - }); - }, - { enabled: !!user }, - ); - const { refetch } = filterQuery; - const { showToast } = useToasts(); - - const handleSave = async () => { - await refetch(); - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const addButton = ( - - - - {close => } - - - ); - - return ( - <> - {showHeader && ( - - {user.role !== ROLES.viewOnly && addButton} - - )} - - {({ data }) => ( - - )} - - - ); -} - -export default WebsitesList; diff --git a/src/components/pages/websites/WebsiteEventDataPage.js b/src/components/pages/websites/WebsiteEventDataPage.js deleted file mode 100644 index 08acafb5..00000000 --- a/src/components/pages/websites/WebsiteEventDataPage.js +++ /dev/null @@ -1,12 +0,0 @@ -import Page from 'components/layout/Page'; -import WebsiteHeader from './WebsiteHeader'; -import WebsiteEventData from './WebsiteEventData'; - -export default function WebsiteEventDataPage({ websiteId }) { - return ( - - - - - ); -} diff --git a/src/components/pages/websites/WebsitesPage.js b/src/components/pages/websites/WebsitesPage.js deleted file mode 100644 index 4c1ee409..00000000 --- a/src/components/pages/websites/WebsitesPage.js +++ /dev/null @@ -1,77 +0,0 @@ -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm'; -import WebsiteList from 'components/pages/settings/websites/WebsitesList'; -import { useMessages } from 'components/hooks'; -import useUser from 'components/hooks/useUser'; -import { ROLES } from 'lib/constants'; -import { useState } from 'react'; -import { - Button, - Icon, - Icons, - Item, - Modal, - ModalTrigger, - Tabs, - Text, - useToasts, -} from 'react-basics'; - -const TABS = { - myWebsites: 'my-websites', - teamWebsites: 'team-websites', -}; - -export function WebsitesPage() { - const { formatMessage, labels, messages } = useMessages(); - const [tab, setTab] = useState(TABS.myWebsites); - const { user } = useUser(); - const { showToast } = useToasts(); - const cloudMode = Boolean(process.env.cloudMode); - - const handleSave = () => { - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }; - - const addButton = ( - <> - {user.role !== ROLES.viewOnly && ( - - - - {close => } - - - )} - - ); - - return ( - - {!cloudMode && addButton} - - {formatMessage(labels.myWebsites)} - {formatMessage(labels.teamWebsites)} - - {tab === TABS.myWebsites && ( - - )} - {tab === TABS.teamWebsites && ( - - )} - - ); -} - -export default WebsitesPage; diff --git a/src/index.ts b/src/index.ts index f2ef13ca..72fe733b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,29 +89,29 @@ export * from 'components/hooks/useUser'; export * from 'components/hooks/useWebsite'; export * from 'components/hooks/useWebsiteReports'; -export * from 'components/pages/settings/teams/TeamAddForm'; -export * from 'components/pages/settings/teams/TeamAddWebsiteForm'; -export * from 'components/pages/settings/teams/TeamDeleteForm'; -export * from 'components/pages/settings/teams/TeamEditForm'; -export * from 'components/pages/settings/teams/TeamJoinForm'; -export * from 'components/pages/settings/teams/TeamLeaveForm'; -export * from 'components/pages/settings/teams/TeamMemberRemoveButton'; -export * from 'components/pages/settings/teams/TeamMembers'; -export * from 'components/pages/settings/teams/TeamMembersTable'; -export * from 'components/pages/settings/teams/TeamSettings'; -export * from 'components/pages/settings/teams/TeamsList'; -export * from 'components/pages/settings/teams/TeamsTable'; -export * from 'components/pages/settings/teams/TeamWebsiteRemoveButton'; -export * from 'components/pages/settings/teams/TeamWebsites'; -export * from 'components/pages/settings/teams/TeamWebsitesTable'; -export * from 'components/pages/settings/teams/WebsiteTags'; +export * from 'app/(app)/settings/teams/TeamAddForm'; +export * from 'app/(app)/settings/teams/[id]/TeamAddWebsiteForm'; +export * from 'app/(app)/settings/teams/TeamDeleteForm'; +export * from 'app/(app)/settings/teams/[id]/TeamEditForm'; +export * from 'app/(app)/settings/teams/TeamJoinForm'; +export * from 'app/(app)/settings/teams/TeamLeaveForm'; +export * from 'app/(app)/settings/teams/[id]/TeamMemberRemoveButton'; +export * from 'app/(app)/settings/teams/[id]/TeamMembers'; +export * from 'app/(app)/settings/teams/[id]/TeamMembersTable'; +export * from 'app/(app)/settings/teams/[id]/TeamSettings'; +export * from 'app/(app)/settings/teams/TeamsList'; +export * from 'app/(app)/settings/teams/TeamsTable'; +export * from 'app/(app)/settings/teams/TeamWebsiteRemoveButton'; +export * from 'app/(app)/settings/teams/[id]/TeamWebsites'; +export * from 'app/(app)/settings/teams/[id]/TeamWebsitesTable'; +export * from 'app/(app)/settings/teams/WebsiteTags'; -export * from 'components/pages/settings/websites/ShareUrl'; -export * from 'components/pages/settings/websites/TrackingCode'; -export * from 'components/pages/settings/websites/WebsiteAddForm'; -export * from 'components/pages/settings/websites/WebsiteDeleteForm'; -export * from 'components/pages/settings/websites/WebsiteEditForm'; -export * from 'components/pages/settings/websites/WebsiteResetForm'; -export * from 'components/pages/settings/websites/WebsiteSettings'; -export * from 'components/pages/settings/websites/WebsitesList'; -export * from 'components/pages/settings/websites/WebsitesTable'; +export * from 'app/(app)/settings/websites/[id]/ShareUrl'; +export * from 'app/(app)/settings/websites/[id]/TrackingCode'; +export * from 'app/(app)/settings/websites/WebsiteAddForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteDeleteForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteEditForm'; +export * from 'app/(app)/settings/websites/[id]/WebsiteResetForm'; +export * from 'app/(app)/settings/websites/WebsiteSettings'; +export * from 'app/(app)/settings/websites/WebsitesList'; +export * from 'app/(app)/settings/websites/WebsitesTable'; diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 4be958b6..e1e2a38b 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -14,7 +14,6 @@ import { } from 'next-basics'; import { NextApiRequestCollect } from 'pages/api/send'; import { getUserById } from '../queries'; -import { NextApiRequestQueryBody } from './types'; const log = debug('umami:middleware'); @@ -83,14 +82,18 @@ export const useAuth = createMiddleware(async (req, res, next) => { next(); }); -export const useValidate = createMiddleware(async (req: any, res, next) => { - try { - const { yup } = req as NextApiRequestQueryBody; +export const useValidate = async (schema, req, res) => { + return createMiddleware(async (req: any, res, next) => { + try { + const rules = schema[req.method]; - yup[req.method] && yup[req.method].validateSync({ ...req.query, ...req.body }); - } catch (e: any) { - return badRequest(res, e.message); - } + if (rules) { + rules.validateSync(req.method === 'GET' ? { ...req.query } : { ...req.body }); + } + } catch (e: any) { + return badRequest(res, e.message); + } - next(); -}); + next(); + })(req, res); +}; diff --git a/src/pages/404.js b/src/pages/404.js deleted file mode 100644 index 8fa13a9c..00000000 --- a/src/pages/404.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Row, Column, Flexbox } from 'react-basics'; -import AppLayout from 'components/layout/AppLayout'; -import useMessages from 'components/hooks/useMessages'; - -export default function Custom404() { - const { formatMessage, labels } = useMessages(); - - return ( - - - - -

{formatMessage(labels.pageNotFound)}

-
-
-
-
- ); -} diff --git a/src/pages/_app.js b/src/pages/_app.js deleted file mode 100644 index 7022772c..00000000 --- a/src/pages/_app.js +++ /dev/null @@ -1,69 +0,0 @@ -import { IntlProvider } from 'react-intl'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ReactBasicsProvider } from 'react-basics'; -import Head from 'next/head'; -import Script from 'next/script'; -import { useRouter } from 'next/router'; -import ErrorBoundary from 'components/common/ErrorBoundary'; -import useLocale from 'components/hooks/useLocale'; -import '@fontsource/inter/400.css'; -import '@fontsource/inter/700.css'; -import 'react-basics/dist/styles.css'; -import 'styles/variables.css'; -import 'styles/locale.css'; -import 'styles/index.css'; -import 'chartjs-adapter-date-fns'; - -const client = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - refetchOnWindowFocus: false, - }, - }, -}); - -export default function App({ Component, pageProps }) { - const { locale, messages } = useLocale(); - const { basePath, pathname } = useRouter(); - - return ( - - null}> - - - - - - - - - - - - - - - - - {!pathname.includes('/share/') &&