diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 879ae10..bb52756 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - name: Wait for Docker Edge image to be updated in registry - run: sleep 15s + run: sleep 45s shell: bash - uses: superfly/flyctl-actions/setup-flyctl@master diff --git a/bun.lockb b/bun.lockb index 2cc50fb..70da5a3 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/core/go.mod b/core/go.mod index e87c544..a59a5f3 100644 --- a/core/go.mod +++ b/core/go.mod @@ -13,9 +13,9 @@ require ( github.com/marcboeker/go-duckdb v1.7.1 github.com/medama-io/go-referrer-parser v0.0.0-20240903120234-0a63376371c3 github.com/medama-io/go-timezone-country v0.0.0-20240903121643-db228bdc5dc1 - github.com/medama-io/go-useragent v0.0.0-20240904185757-43ce2229c0b3 + github.com/medama-io/go-useragent v1.0.0 github.com/ncruces/go-sqlite3 v0.18.2 - github.com/ogen-go/ogen v1.3.0 + github.com/ogen-go/ogen v1.4.0 github.com/rs/cors v1.11.1 github.com/rs/zerolog v1.33.0 github.com/shirou/gopsutil/v4 v4.24.8 @@ -67,10 +67,10 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/tools v0.24.0 // indirect diff --git a/core/go.sum b/core/go.sum index 509ebc2..c8389e8 100644 --- a/core/go.sum +++ b/core/go.sum @@ -78,16 +78,16 @@ github.com/medama-io/go-referrer-parser v0.0.0-20240903120234-0a63376371c3 h1:6/ github.com/medama-io/go-referrer-parser v0.0.0-20240903120234-0a63376371c3/go.mod h1:Zng9ySjx7KXIpvVqT/mZbYfKE39CkyS/aQR4kXdJuG0= github.com/medama-io/go-timezone-country v0.0.0-20240903121643-db228bdc5dc1 h1:/Q1ZWbdGSRpExJRlQZybxwxXa6u4lYH7K/OLD9t/d8M= github.com/medama-io/go-timezone-country v0.0.0-20240903121643-db228bdc5dc1/go.mod h1:Wq7lg5D0ZdQ3bHnzOTKsb1YGlxm/l82OVA4aIbAA5w4= -github.com/medama-io/go-useragent v0.0.0-20240904185757-43ce2229c0b3 h1:Drn4ysoeSkdEMIkHpW8oapLKxLxJdJV/Oticg8XR8Gk= -github.com/medama-io/go-useragent v0.0.0-20240904185757-43ce2229c0b3/go.mod h1:H9GYWth4IN8vAFZh5LeARza7VwM4jK9uk7Tb9huVzLw= +github.com/medama-io/go-useragent v1.0.0 h1:AEtLVKtUV1PiZ4u/zCkRLHBahs/crkoFw/YLSU3Rb8w= +github.com/medama-io/go-useragent v1.0.0/go.mod h1:H9GYWth4IN8vAFZh5LeARza7VwM4jK9uk7Tb9huVzLw= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/ncruces/go-sqlite3 v0.18.2 h1:m7QXhBWIwXsp84HE11t+ze0n1v3LRU+zGFg4uHjBeFA= github.com/ncruces/go-sqlite3 v0.18.2/go.mod h1:4sZHOm+b/FM8FJRVGN4TemkPPDq5JXGK/1EHIEWxsYo= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= -github.com/ogen-go/ogen v1.3.0 h1:c0+CvdbwvKmaHQUqbPpRKflvkiJ/NAsEw3L3HhofDso= -github.com/ogen-go/ogen v1.3.0/go.mod h1:421U7mQVAE+7uaCc4tkq2uT0HDfZL13UTpL16CBrFt0= +github.com/ogen-go/ogen v1.4.0 h1:8xNMwpvQwCxPTL2hypn60qnjiijoVzxlB9j/MOf/34U= +github.com/ogen-go/ogen v1.4.0/go.mod h1:YeliH7gAS6QToqDqIM5BrnEUOiXiqCnNqNwzEpebDsY= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -149,10 +149,10 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= -golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= @@ -162,8 +162,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/core/services/event.go b/core/services/event.go index f8f0ff9..6c660a0 100644 --- a/core/services/event.go +++ b/core/services/event.go @@ -166,18 +166,18 @@ func (h *Handler) PostEventHit(ctx context.Context, req api.EventHit, _params ap ua := h.useragent.Parse(rawUserAgent) // If the user agent is a bot, we want to ignore it. - if ua.Bot { + if ua.IsBot() { log.Debug().Str("user_agent", rawUserAgent).Msg("hit: user agent is a bot") return &api.PostEventHitNoContent{}, nil } - uaBrowser := ua.Browser + uaBrowser := ua.GetBrowser() if uaBrowser == "" { uaBrowser = Unknown unknownCounter++ } - uaOS := ua.OS + uaOS := ua.GetOS() if uaOS == "" { uaOS = Unknown unknownCounter++ @@ -185,13 +185,13 @@ func (h *Handler) PostEventHit(ctx context.Context, req api.EventHit, _params ap uaDevice := Unknown switch { - case ua.Desktop: + case ua.IsDesktop(): uaDevice = "Desktop" - case ua.Mobile: + case ua.IsMobile(): uaDevice = "Mobile" - case ua.Tablet: + case ua.IsTablet(): uaDevice = "Tablet" - case ua.TV: + case ua.IsTV(): uaDevice = "TV" default: unknownCounter++ diff --git a/dashboard/app/components/Button.tsx b/dashboard/app/components/Button.tsx index 0e68a6d..bbcaf5c 100644 --- a/dashboard/app/components/Button.tsx +++ b/dashboard/app/components/Button.tsx @@ -73,6 +73,7 @@ const ButtonLink = ({ ? 'button-outline' : 'button-link' } + prefetch="intent" role="button" {...rest} > diff --git a/dashboard/app/components/DropdownSelect.module.css b/dashboard/app/components/DropdownSelect.module.css index 61cf258..5de4e75 100644 --- a/dashboard/app/components/DropdownSelect.module.css +++ b/dashboard/app/components/DropdownSelect.module.css @@ -85,7 +85,8 @@ } &:focus-visible { - outline: 2px solid var(--focus-outline); + background-color: var(--bg-grey-blue-dark); + outline: none; } &[data-state="checked"] { diff --git a/dashboard/app/components/ScrollArea.module.css b/dashboard/app/components/ScrollArea.module.css index 60a3bdf..4394c83 100644 --- a/dashboard/app/components/ScrollArea.module.css +++ b/dashboard/app/components/ScrollArea.module.css @@ -1,7 +1,8 @@ .root { + height: 100%; width: 100%; overflow: hidden; - --scrollbar-size: 10px; + --scrollbar-size: 11px; } .viewport { @@ -17,11 +18,11 @@ /* disable browser handling of all panning and zooming gestures on touch devices */ touch-action: none; padding: 2px; - background: var(--black-a1); + background: transparent; transition: background 160ms ease-out; &:hover { - background: var(--black-a3); + background: var(--black-a1); } &[data-orientation="vertical"] { @@ -36,7 +37,7 @@ .thumb { flex: 1; - background: var(--black-a3); + background: #0006; border-radius: var(--scrollbar-size); position: relative; diff --git a/dashboard/app/components/Tooltip.module.css b/dashboard/app/components/Tooltip.module.css index c3975fa..9b2bb55 100644 --- a/dashboard/app/components/Tooltip.module.css +++ b/dashboard/app/components/Tooltip.module.css @@ -1,36 +1,39 @@ .content { - border-radius: 4px; - padding: 10px 15px; - font-size: 15px; - line-height: 1; - color: var(--violet-11); - background-color: white; + padding: 8px 12px; + + color: var(--text-light); + background-color: var(--bg-grey-blue); box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px; + + border: 1px solid var(--bg-grey-blue-dark); + border-radius: 4px; + + line-height: 1; user-select: none; animation-duration: 400ms; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); will-change: transform, opacity; &[data-state="delayed-open"][data-side="top"] { - animation-name: slideUpAndFade; + animation-name: slideDownAndFade; } &[data-state="delayed-open"][data-side="right"] { - animation-name: slideRightAndFade; + animation-name: slideLeftAndFade; } &[data-state="delayed-open"][data-side="bottom"] { - animation-name: slideDownAndFade; + animation-name: slideUpAndFade; } &[data-state="delayed-open"][data-side="left"] { - animation-name: slideLeftAndFade; + animation-name: slideRightAndFade; } } .arrow { - fill: white; + fill: var(--bg-grey-blue); } @keyframes slideUpAndFade { diff --git a/dashboard/app/components/Tooltip.tsx b/dashboard/app/components/Tooltip.tsx index ae20e05..1c2b61c 100644 --- a/dashboard/app/components/Tooltip.tsx +++ b/dashboard/app/components/Tooltip.tsx @@ -4,23 +4,50 @@ import classes from './Tooltip.module.css'; interface TooltipProps { children: React.ReactNode; - label: string; + content: string; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; + + contentClassname?: string; + arrowClassname?: string; } -const Tooltip = ({ children, label }: TooltipProps) => { +const Tooltip = ({ + children, + content, + open, + defaultOpen, + onOpenChange, + contentClassname, + arrowClassname, + ...props +}: TooltipProps) => { return ( - - - {children} - - - {label} - - - - - + + {children} + + {content} + + + ); }; -export { Tooltip }; +const TooltipProvider = TooltipPrimitive.Provider; + +export { Tooltip, TooltipProvider }; diff --git a/dashboard/app/components/layout/Error.module.css b/dashboard/app/components/layout/Error.module.css index a9cb86f..ee8390b 100644 --- a/dashboard/app/components/layout/Error.module.css +++ b/dashboard/app/components/layout/Error.module.css @@ -19,6 +19,12 @@ max-width: 500px; margin: auto; margin-bottom: 30px; + + color: var(--text-muted); + font-size: 18px; + text-align: center; + + line-height: 1.5; } .title { @@ -42,3 +48,10 @@ color: var(--text-dark); } } + +.error { + font-size: 18px; + color: var(--text-red); + + line-height: 1.5; +} diff --git a/dashboard/app/components/layout/Error.tsx b/dashboard/app/components/layout/Error.tsx index 9102e1f..affde63 100644 --- a/dashboard/app/components/layout/Error.tsx +++ b/dashboard/app/components/layout/Error.tsx @@ -1,4 +1,3 @@ -import { Anchor, Container, Text } from '@mantine/core'; import { Link } from '@remix-run/react'; import type { ReactNode } from 'react'; @@ -14,19 +13,34 @@ interface InternalServerErrorProps { error?: string; } +interface ErrorProps { + message?: string; +} + +class StatusError extends Error { + status: number; + + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} + +const isStatusError = (error: unknown): error is StatusError => { + return error instanceof StatusError; +}; + const ErrorPage = ({ label, title, description }: ErrorPageProps) => { return ( - +
{label}

{title}

- - {description} - - +

{description}

+
); }; -const BadRequestError = () => ( +const BadRequestError = ({ message }: ErrorProps) => ( ( <> Please check the URL and try again. If you think this is an error, please{' '} - report this issue - {' '} + {' '} to the developers. + {message && ( +
+
+ + {message ? `Error: ${message}` : ''} + +
+ )} } /> @@ -80,16 +103,16 @@ const NotFoundError = () => ( description={ <> The page you're looking for isn't here. Feel free to return to the{' '} - + home page - {' '} + {' '} or browse the docs to find what you need. } /> ); -const ConflictError = () => ( +const ConflictError = ({ message }: ErrorProps) => ( ( <> There was a conflict while processing your request. Did you try to create something that already exists? + {message && ( +
+
+ + {message ? `Error: ${message}` : ''} + +
+ )} } /> @@ -109,18 +140,21 @@ const InternalServerError = ({ error }: InternalServerErrorProps) => ( description={ <> We encountered an unexpected error while processing your request. Please{' '} - report this issue - {' '} + {' '} to the developers. -
- - {error ? `Error: ${error}` : ''} - +
+
+ + {error ? `Error: ${error}` : ''} + +
} /> @@ -132,4 +166,6 @@ export { ForbiddenError, InternalServerError, NotFoundError, + StatusError, + isStatusError, }; diff --git a/dashboard/app/components/layout/Header.tsx b/dashboard/app/components/layout/Header.tsx index 98cb484..b2cbdf0 100644 --- a/dashboard/app/components/layout/Header.tsx +++ b/dashboard/app/components/layout/Header.tsx @@ -138,7 +138,7 @@ export const Header = () => {
- + diff --git a/dashboard/app/components/login/Login.module.css b/dashboard/app/components/login/Login.module.css index f1e8542..24ba752 100644 --- a/dashboard/app/components/login/Login.module.css +++ b/dashboard/app/components/login/Login.module.css @@ -5,6 +5,7 @@ width: 380px; justify-content: center; + border: 1px solid var(--border-muted); border-radius: 8px; @media (--lt-xs) { diff --git a/dashboard/app/components/login/Login.tsx b/dashboard/app/components/login/Login.tsx index 61ebd56..2f97eb8 100644 --- a/dashboard/app/components/login/Login.tsx +++ b/dashboard/app/components/login/Login.tsx @@ -1,4 +1,3 @@ -import { Paper } from '@mantine/core'; import { useForm } from '@mantine/form'; import { Form, useSubmit } from '@remix-run/react'; import { valibotResolver } from 'mantine-form-valibot-resolver'; @@ -36,7 +35,7 @@ export const Login = () => { }; return ( - +

Log in to your dashboard

@@ -79,6 +78,6 @@ export const Login = () => { - +
); }; diff --git a/dashboard/app/components/settings/Sidebar.tsx b/dashboard/app/components/settings/Sidebar.tsx index a2fb631..b964c58 100644 --- a/dashboard/app/components/settings/Sidebar.tsx +++ b/dashboard/app/components/settings/Sidebar.tsx @@ -17,6 +17,7 @@ export const Sidebar = () => { return ( diff --git a/dashboard/app/components/settings/WebsiteSelector.module.css b/dashboard/app/components/settings/WebsiteSelector.module.css index adb4ac6..c70aec6 100644 --- a/dashboard/app/components/settings/WebsiteSelector.module.css +++ b/dashboard/app/components/settings/WebsiteSelector.module.css @@ -1,12 +1,22 @@ -.target { +.trigger { + composes: button from global; + min-width: 192px; height: 40px; - padding: 0 16px; + padding: 0 12px 0 16px; + background-color: var(--bg-light); + color: var(--text-dark); + border: 1px solid var(--border-light); border-radius: 8px; -} -.targetWrapper { + flex-wrap: 1; + justify-content: space-between; + + &:hover { + background-color: var(--bg-light); + } + @media (--lt-xs) { width: 100%; margin: 4px 0 16px 0; @@ -14,13 +24,45 @@ } .dropdown { + height: 100%; + width: var(--radix-dropdown-menu-trigger-width); + + background-color: white; + border: 1px solid var(--border-muted); border-radius: 8px; + + &[data-scroll="true"] { + height: 180px; + } } -.option { - padding: 8px 16px; +.item { + composes: button from global; + + padding: 7px 14px; + height: fit-content; + width: 100%; + text-align: left; + justify-content: flex-start; + + color: var(--text-dark); + background-color: white; + + border: none; + border-radius: 8px; + + &:focus-visible { + background-color: var(--black-a1); + border-radius: 0; + outline: none; + } + + &:hover { + background-color: var(--black-a1); + border-radius: 0; + } - &[data-combobox-active="true"] { + &[data-active="true"] { font-weight: 600; } } diff --git a/dashboard/app/components/settings/WebsiteSelector.tsx b/dashboard/app/components/settings/WebsiteSelector.tsx index 422025a..19b3cb1 100644 --- a/dashboard/app/components/settings/WebsiteSelector.tsx +++ b/dashboard/app/components/settings/WebsiteSelector.tsx @@ -1,5 +1,8 @@ -import { Combobox, InputBase, ScrollArea, useCombobox } from '@mantine/core'; -import { useCallback, useMemo } from 'react'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { ChevronDownIcon } from '@radix-ui/react-icons'; +import { useMemo } from 'react'; + +import { ScrollArea } from '@/components/ScrollArea'; import classes from './WebsiteSelector.module.css'; @@ -14,71 +17,48 @@ export const WebsiteSelector = ({ website, setWebsite, }: WebsiteListComboboxProps) => { - const combobox = useCombobox({ - onDropdownClose: () => { - combobox.resetSelectedOption(); - }, - onDropdownOpen: (eventSource) => { - if (eventSource === 'keyboard') { - combobox.selectActiveOption(); - } else { - combobox.updateSelectedOptionIndex('active'); - } - }, - }); - - const handleOptionSubmit = useCallback( - (value: string) => { - setWebsite(value); - combobox.toggleDropdown(); - }, - [setWebsite, combobox.toggleDropdown], - ); - const options = useMemo( () => websites.map((value) => ( - setWebsite(value)} + asChild > - {value} - + + )), - [websites, website], + [websites, website, setWebsite], ); return ( - - - + + + + + 5} + > + {options} + + ); }; diff --git a/dashboard/app/components/stats/Chart.module.css b/dashboard/app/components/stats/Chart.module.css new file mode 100644 index 0000000..70eb9dd --- /dev/null +++ b/dashboard/app/components/stats/Chart.module.css @@ -0,0 +1,22 @@ +.tooltip { + padding: 22px 16px; + + background-color: var(--bg-light); + border: 1px solid var(--border-light); + border-radius: 8px; + + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; +} + +.date { + font-weight: 500; + line-height: 1; + margin-bottom: 16px; +} diff --git a/dashboard/app/components/stats/Chart.tsx b/dashboard/app/components/stats/Chart.tsx index 11a07a7..c8b6943 100644 --- a/dashboard/app/components/stats/Chart.tsx +++ b/dashboard/app/components/stats/Chart.tsx @@ -4,13 +4,16 @@ import { AreaChart as MantineAreaChart, BarChart as MantineBarChart, } from '@mantine/charts'; -import { ColorSwatch, Group, Paper, Text } from '@mantine/core'; import { useSearchParams } from '@remix-run/react'; import { format, parseISO } from 'date-fns'; import React, { useMemo } from 'react'; +import { Group } from '@/components/layout/Flex'; + import { formatCount, formatDuration, formatPercentage } from './formatter'; +import classes from './Chart.module.css'; + interface ChartData { date: string; value: number; @@ -122,18 +125,19 @@ const ChartTooltip = React.memo( }, [dateTimeFormat, date, period]); return ( - - - {dateLabel} - - - - - {label} +
+

{dateLabel}

+ + +
+ {label} - {valueFormatter(item.value)} + {valueFormatter(item.value)} - +
); }, ); diff --git a/dashboard/app/components/stats/Filter.module.css b/dashboard/app/components/stats/Filter.module.css index c16b565..1889a7d 100644 --- a/dashboard/app/components/stats/Filter.module.css +++ b/dashboard/app/components/stats/Filter.module.css @@ -1,52 +1,138 @@ +.root { + composes: group from global; + + margin-top: -40px; + justify-content: flex-start; + gap: 16px; + flex-wrap: wrap; +} + .popover { + width: 494px; padding: 16px 16px 12px 16px; gap: 16px; + background-color: var(--bg-light); + border: 1px solid var(--border-light); border-radius: 8px; -} -.select { - margin: 16px -16px 0 -16px; - padding: 12px 16px 0 16px; - border-top: 1px solid var(--border-light); + h5 { + font-size: 16px; + font-weight: 600; + padding-bottom: 8px; + } } -.dropdown { - width: 120px; - height: 40px; - border-radius: 8px; +.trigger { + composes: button from global; + + flex: 1 1 0; + width: 0; + height: 36px; + padding: 0 10px 0 14px; + justify-content: space-between; + + color: var(--text-dark); + background-color: white; + + border: 1px solid var(--border-light); + border-radius: 4px; + + &:hover { + background-color: var(--text-light); + } + + &:focus, + &:focus-visible { + outline: 1px solid var(--focus-outline); + outline-offset: -1px; + } svg { + margin-left: 4px; color: var(--text-dark); } } -.dropdown-label { - display: block; +.input { + flex: 1 1 0; + width: 0; + height: 36px; + padding: 0 10px 0 14px; + + color: var(--text-dark); + background-color: white; + + border: 1px solid var(--border-light); + border-radius: 4px; + + &:hover { + background-color: var(--text-light); + } + + &:focus, + &:focus-visible { + outline: 1px solid var(--focus-outline); + outline-offset: -1px; + } +} + +.label { white-space: nowrap; overflow: hidden; - text-overflow: clip; + text-overflow: ellipsis; + line-height: normal; } -.cancel { - display: flex; - align-items: center; - justify-content: center; +.dropdown-list { + composes: group from global; - width: 80px; - height: 40px; + gap: 8px; +} - font-size: 14px; - font-weight: 600; +.dropdown { + height: 200px; + width: var(--radix-dropdown-menu-trigger-width); - border: 1px solid var(--border-light); - border-radius: 8px; + background-color: white; + border: 1px solid var(--border-muted); + border-radius: 4px; +} + +.item { + composes: button from global; + + padding: 6px 14px; + height: fit-content; + width: 100%; + text-align: left; + justify-content: flex-start; + + color: var(--text-dark); + background-color: white; + + border: none; + border-radius: 4px; + + &:focus-visible { + background-color: var(--black-a1); + border-radius: 0; + outline: none; + } + + &:hover { + background-color: var(--black-a1); + border-radius: 0; + } } .filter-item { - height: 40px; + composes: group from global; + + min-height: 40px; padding: 0 8px 0 16px; + gap: 0; color: var(--text-grey); background-color: var(--selected-light); @@ -54,32 +140,100 @@ border-radius: 8px; cursor: default; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: normal; + + span { + padding: 8px 0; + } + + div { + display: inline-flex; + align-items: center; + + margin-left: 4px; + padding: 6px; + + border-radius: 7px; + + cursor: pointer; + + &:hover { + background-color: var(--black-a1); + } + + svg { + height: 15px; + width: 15px; + } + } } /* Buttons */ .add { + composes: button from global; + width: 120px; height: 40px; + align-items: center; + gap: 8px; color: var(--text-light); background-color: var(--teal); - border-radius: 8px; + border: none; + + &:hover { + background-color: var(--teal); + } + + span { + font-weight: 600; + } } -.apply { - display: flex; - align-items: center; - justify-content: center; +.select { + composes: group from global; - width: 80px; - height: 40px; + margin: 16px -16px 0 -16px; + padding: 12px 16px 0 16px; + gap: 12px; + + justify-content: flex-end; - font-size: 14px; + border-top: 1px solid var(--border-light); +} + +.apply { + composes: button from global; + width: 80px; font-weight: 600; color: var(--text-light); background-color: var(--logo-green); + border: 1px solid transparent; + border-radius: 8px; + + &:hover { + background-color: var(--logo-green); + } +} + +.cancel { + composes: button from global; + width: 80px; + font-weight: 600; + + color: var(--text-dark); + background-color: var(--bg-light); + + border: 1px solid var(--border-light); border-radius: 8px; + + &:hover { + background-color: var(--bg-light); + } } diff --git a/dashboard/app/components/stats/Filter.tsx b/dashboard/app/components/stats/Filter.tsx index 4be8272..a2892e2 100644 --- a/dashboard/app/components/stats/Filter.tsx +++ b/dashboard/app/components/stats/Filter.tsx @@ -1,23 +1,10 @@ -import { - CloseButton, - Combobox, - Group, - InputBase, - Popover, - ScrollArea, - Text, - TextInput, - UnstyledButton, - useCombobox, -} from '@mantine/core'; -import { - ChevronDownIcon, - ChevronUpIcon, - PlusIcon, -} from '@radix-ui/react-icons'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { ChevronDownIcon, Cross1Icon, PlusIcon } from '@radix-ui/react-icons'; +import * as Popover from '@radix-ui/react-popover'; import { useSearchParams } from '@remix-run/react'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ScrollArea } from '@/components/ScrollArea'; import { useFilter } from '@/hooks/use-filter'; import type { Filter, FilterOperator } from './types'; @@ -114,67 +101,33 @@ interface FilterDropdownProps { } const FilterDropdown = ({ choices, value, setValue }: FilterDropdownProps) => { - const combobox = useCombobox({ - onDropdownClose: () => { - combobox.resetSelectedOption(); - }, - onDropdownOpen: (eventSource) => { - if (eventSource === 'keyboard') { - combobox.selectActiveOption(); - } else { - combobox.updateSelectedOptionIndex('active'); - } - }, - }); - const label = isFilterType(choices) ? choices[value as FilterOperator]?.label : choices[value as Filter]?.label; - const options = Object.entries(choices).map(([key, filter]) => ( - - {filter.label} - - )); + const options = useMemo(() => { + return Object.entries(choices).map(([key, filter]) => ( + setValue(key)} asChild> + + + )); + }, [choices, setValue]); return ( - { - setValue(option); - combobox.updateSelectedOptionIndex('active'); - combobox.closeDropdown(); - }} - store={combobox} - withinPortal={false} - > - - : - } - rightSectionPointerEvents="none" - onClick={() => { - combobox.toggleDropdown(); - }} - > - - {label ?? 'Unknown'} - - - + + + + - - - - {options} - - - - + + {options} + + ); }; @@ -239,16 +192,15 @@ export const Filters = () => { ); return ( - - + - - + + + + +
New filter
+
+ void} + /> + void} + /> + { + setValue(event.currentTarget.value); + }} + onKeyDown={(event) => { + if (event.key === 'Enter' && value !== '') { + handleAddFilters(); + } + }} + placeholder={chosenFilter?.placeholder} + /> +
+
+ + +
+
+
+
{paramsArr.map(([label, type, value]) => { return ( - - {label}  - +
+ {label}  + {FILTER_TYPES[type as FilterOperator]?.label ?? 'Unknown'}  - - {value} - - + + {value} +
+ +
+
); })} -
+
); }; diff --git a/dashboard/app/components/stats/HeaderDataBox.module.css b/dashboard/app/components/stats/HeaderDataBox.module.css index 9adb9ad..a262d91 100644 --- a/dashboard/app/components/stats/HeaderDataBox.module.css +++ b/dashboard/app/components/stats/HeaderDataBox.module.css @@ -1,17 +1,22 @@ .databox { + display: block; height: 132px; width: 192px; padding: 12px 24px 24px 24px; position: relative; + color: var(--text-disabled); + background-color: transparent; + border: 1px solid transparent; - cursor: pointer; + border-radius: 8px; /* Used to prevent wrapping on overflow */ flex: "0 0 auto"; + text-align: left; - border-radius: 8px; transition: all 0.1s ease; + cursor: pointer; &[data-active="true"] { color: var(--text-light); @@ -22,6 +27,11 @@ color: var(--text-light); border: 1px solid var(--border-dark); } + + &:focus-visible { + outline-offset: -1px; + outline: 2px solid var(--focus-outline); + } } .label { diff --git a/dashboard/app/components/stats/HeaderDataBox.tsx b/dashboard/app/components/stats/HeaderDataBox.tsx index a87cbb7..0532125 100644 --- a/dashboard/app/components/stats/HeaderDataBox.tsx +++ b/dashboard/app/components/stats/HeaderDataBox.tsx @@ -1,7 +1,8 @@ -import { Box, Group, Tooltip, UnstyledButton } from '@mantine/core'; import { useSearchParams } from '@remix-run/react'; import React, { useMemo } from 'react'; +import { Tooltip, TooltipProvider } from '@/components/Tooltip'; +import { Group } from '@/components/layout/Flex'; import { useChartType } from '@/hooks/use-chart-type'; import { formatCount, formatDuration, formatPercentage } from './formatter'; @@ -86,36 +87,43 @@ const HeaderDataBox = React.memo(({ stat, isActive }: HeaderDataBoxProps) => { }; return ( - - - {formattedValue} - -

{stat.label}

- + + + + ); }); diff --git a/dashboard/app/components/stats/StatsHeader.module.css b/dashboard/app/components/stats/StatsHeader.module.css index f713e68..27a3c21 100644 --- a/dashboard/app/components/stats/StatsHeader.module.css +++ b/dashboard/app/components/stats/StatsHeader.module.css @@ -1,4 +1,5 @@ .title { + display: flex; padding: 12px 0; justify-content: space-between; @@ -13,14 +14,33 @@ } .dropdowns { + composes: group from global; + align-items: center; + gap: 16px; @media (--lt-xs) { + flex-wrap: wrap; gap: 8px; width: 100%; } } +.scrollcontainer { + @media (--gt-lg) { + user-select: none; + cursor: default; + } +} + +.scrollgroup { + composes: group from global; + + flex-wrap: nowrap; + padding: 4px; + gap: 16px; +} + /* Chart Toggle */ .toggle { padding: 4px; @@ -37,14 +57,24 @@ } .control { - color: var(--text-disabled); width: 38px; height: 38px; + color: var(--text-disabled); + background-color: transparent; + border: none; + + cursor: pointer; + &[data-active="true"], &:hover { color: var(--text-light); } + + &[data-active="true"] { + background-color: var(--logo-green); + border-radius: 50%; + } } .controlLabel { @@ -60,8 +90,3 @@ height: 20px; } } - -.indicator { - background-color: var(--logo-green); - border-radius: 50%; -} diff --git a/dashboard/app/components/stats/StatsHeader.tsx b/dashboard/app/components/stats/StatsHeader.tsx index adb88ae..c04ec62 100644 --- a/dashboard/app/components/stats/StatsHeader.tsx +++ b/dashboard/app/components/stats/StatsHeader.tsx @@ -1,20 +1,16 @@ -import { - Flex, - FloatingIndicator, - Group, - Tooltip, - UnstyledButton, -} from '@mantine/core'; import { CalendarIcon } from '@radix-ui/react-icons'; +import * as ToggleGroup from '@radix-ui/react-toggle-group'; import { useParams, useSearchParams } from '@remix-run/react'; import type React from 'react'; -import { useState } from 'react'; +import { Fragment } from 'react'; import { ScrollContainer } from 'react-indiana-drag-scroll'; import { DatePickerRange, datePickerClasses } from '@/components/DatePicker'; import { DropdownSelect } from '@/components/DropdownSelect'; +import { Tooltip, TooltipProvider } from '@/components/Tooltip'; import { IconAreaChart } from '@/components/icons/area'; import { IconBarChart } from '@/components/icons/bar'; +import { Group } from '@/components/layout/Flex'; import { InnerHeader } from '@/components/layout/InnerHeader'; import { useChartType } from '@/hooks/use-chart-type'; import { useDisclosure } from '@/hooks/use-disclosure'; @@ -47,12 +43,6 @@ const CHART_TYPES = [ ] as const; const SegmentedChartControl = () => { - // Segmented control for chart type - const [rootRef, setRootRef] = useState(null); - const [controlsRefs, setControlsRefs] = useState< - Record - >({}); - const { setChartType, getChartType } = useChartType(); const chartType = getChartType(); @@ -60,36 +50,40 @@ const SegmentedChartControl = () => { setChartType(value); }; - const setControlRef = (type: ChartType) => (node: HTMLButtonElement) => { - controlsRefs[type] = node; - setControlsRefs(controlsRefs); - }; - const chartTypes = CHART_TYPES.map((item) => ( - - handleChartChange(item.value)} - data-active={chartType === item.value} - > - {item.icon} - - + + + + + + + )); return ( -
- {chartTypes} - -
+ +
+ {chartTypes} +
+
); }; @@ -133,9 +127,9 @@ const StatsHeader = ({ stats, chart, websites }: StatsHeaderProps) => { return ( - +

Dashboard

- +
{!hideWebsiteSelector && ( { separatorValues={DATE_GROUP_END_VALUES} /> - - - - - +
+
+ + +
{stats.map((stat) => ( { isActive={chart === stat.chart} /> ))} - +
diff --git a/dashboard/app/components/stats/StatsItem.module.css b/dashboard/app/components/stats/StatsItem.module.css index fcc5993..373312d 100644 --- a/dashboard/app/components/stats/StatsItem.module.css +++ b/dashboard/app/components/stats/StatsItem.module.css @@ -4,6 +4,7 @@ padding: 7px 16px; width: calc(100% - 16px); + color: var(--text-dark); background-color: transparent; border: none; border-radius: 8px; diff --git a/dashboard/app/root.tsx b/dashboard/app/root.tsx index 95cafaf..1210452 100644 --- a/dashboard/app/root.tsx +++ b/dashboard/app/root.tsx @@ -34,7 +34,6 @@ import '@mantine/core/styles/Skeleton.css'; import '@mantine/core/styles/Tooltip.css'; // Misc import '@mantine/core/styles/Modal.css'; -import '@mantine/core/styles/ColorSwatch.css'; import '@mantine/core/styles/Table.css'; import '@mantine/core/styles/Text.css'; import '@mantine/core/styles/Anchor.css'; @@ -74,6 +73,7 @@ import { ForbiddenError, InternalServerError, NotFoundError, + isStatusError, } from '@/components/layout/Error'; import theme from '@/styles/theme'; import { EXPIRE_LOGGED_IN, hasSession } from '@/utils/cookies'; @@ -216,6 +216,45 @@ export const ErrorBoundary = () => { ); } + if (isStatusError(error)) { + switch (error.status) { + case 400: { + return ( + + + + ); + } + case 403: { + return ( + + + + ); + } + case 404: { + return ( + + + + ); + } + case 409: { + return ( + + + + ); + } + } + + return ( + + + + ); + } + if (error instanceof Error) { // If the error is due to a loader mismatch, reload the page as it may be // related to a bad cookie cache from the API restarting. This is probably diff --git a/dashboard/app/routes/_index.tsx b/dashboard/app/routes/_index.tsx index 4247c9b..6a97fc1 100644 --- a/dashboard/app/routes/_index.tsx +++ b/dashboard/app/routes/_index.tsx @@ -1,5 +1,5 @@ import { ModalChild, ModalWrapper } from '@/components/Modal'; -import { Paper, SimpleGrid, Text } from '@mantine/core'; +import { SimpleGrid } from '@mantine/core'; import { useForm } from '@mantine/form'; import { PlusIcon } from '@radix-ui/react-icons'; import { @@ -128,9 +128,18 @@ export default function Index() {
{websites.length === 0 && ( - - No websites found. Please add a website! - +
+

+ No websites found. Please add a website! +

+
)} {websites.map((website) => ( diff --git a/dashboard/app/styles/Button.css b/dashboard/app/styles/Button.css index 6bbcb0f..a40fd10 100644 --- a/dashboard/app/styles/Button.css +++ b/dashboard/app/styles/Button.css @@ -25,6 +25,7 @@ } &:focus-visible { + outline-offset: -1px; outline: 2px solid var(--focus-outline); } diff --git a/dashboard/app/styles/global.css b/dashboard/app/styles/global.css index 0998da3..e2436bf 100644 --- a/dashboard/app/styles/global.css +++ b/dashboard/app/styles/global.css @@ -5,6 +5,7 @@ --text-disabled: #8e9cb4; --text-grey: #39414e; --text-muted: #7e8c94; + --text-red: #fa5252; /* Background Colors */ --bg-light: #fcfdfe; @@ -40,7 +41,7 @@ --black-a6: color(display-p3 0 0 0 / 0.4); /* rgba(0, 0, 0, 0.4); */ --black-a7: color(display-p3 0 0 0 / 0.5); /* rgba(0, 0, 0, 0.5); */ --black-a8: color(display-p3 0 0 0 / 0.6); /* rgba(0, 0, 0, 0.6); */ - /* --black-a9: color(display-p3 0 0 0 / 0.7); /* rgba(0, 0, 0, 0.7); */ + --black-a9: color(display-p3 0 0 0 / 0.7); /* rgba(0, 0, 0, 0.7); */ --black-a10: color(display-p3 0 0 0 / 0.8); /* rgba(0, 0, 0, 0.8); */ --black-a11: color(display-p3 0 0 0 / 0.9); /* rgba(0, 0, 0, 0.9); */ --black-a12: color(display-p3 0 0 0 / 0.95); /* rgba(0, 0, 0, 0.95);*/ @@ -109,6 +110,7 @@ } body { + margin: 0; display: flex; min-height: 100vh; flex-direction: column; @@ -118,6 +120,24 @@ body { font-family: var(--default-font-family); } +*, +*::before, +*::after { + box-sizing: border-box; +} + +input, +button, +textarea, +select { + font: inherit; +} + +button, +select { + text-transform: none; +} + main { max-width: 1440px; width: 100%; @@ -171,7 +191,9 @@ h6 { a, a:visited, a:hover, -a:active { +a:active, +button, +button:visited { color: inherit; } diff --git a/dashboard/app/utils/filters.ts b/dashboard/app/utils/filters.ts index f2604fb..3f081de 100644 --- a/dashboard/app/utils/filters.ts +++ b/dashboard/app/utils/filters.ts @@ -13,6 +13,8 @@ import { sub, } from 'date-fns'; +import { StatusError } from '@/components/layout/Error'; + interface FilterOptions { start?: string; end?: string; @@ -94,7 +96,7 @@ const generatePeriods = (searchParams: URLSearchParams) => { startPeriod = startOfHour(sub(currentDate, { hours })); endPeriod = endOfHour(currentDate); } else { - throw new Error(`Invalid period: ${period}`); + throw new StatusError(400, `Invalid time period: ${period}`); } } } diff --git a/dashboard/package.json b/dashboard/package.json index 3750252..84acd77 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -13,12 +13,12 @@ "typecheck": "tsc" }, "dependencies": { - "@ariakit/react": "^0.4.10", + "@ariakit/react": "^0.4.11", "@fontsource-variable/inter": "^5.0.20", - "@mantine/charts": "^7.12.1", - "@mantine/core": "^7.12.1", - "@mantine/form": "^7.12.1", - "@mantine/notifications": "^7.12.1", + "@mantine/charts": "^7.12.2", + "@mantine/core": "^7.12.2", + "@mantine/form": "^7.12.2", + "@mantine/notifications": "^7.12.2", "@radix-ui/react-accessible-icon": "^1.1.0", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1", @@ -31,6 +31,7 @@ "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-visually-hidden": "^1.1.0", "@remix-run/react": "^2.11.2", @@ -38,7 +39,7 @@ "byte-size": "^9.0.0", "date-fns": "^3.6.0", "isbot": "^5.1.17", - "mantine-datatable": "^7.11.3", + "mantine-datatable": "^7.12.4", "mantine-form-valibot-resolver": "^2.0.1", "match-sorter": "^6.3.4", "react": "18.3.0-canary-8039e6d0b-20231026", @@ -46,20 +47,20 @@ "react-dom": "18.3.0-canary-8039e6d0b-20231026", "react-indiana-drag-scroll": "^3.0.3-alpha", "recharts": "^2.13.0-alpha.4", - "valibot": "^0.39.0", + "valibot": "^0.41.0", "validator": "^13.12.0" }, "devDependencies": { "@remix-run/dev": "^2.11.2", "@types/byte-size": "^8.1.2", - "@types/react": "^18.3.4", + "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", - "@types/validator": "^13.12.0", + "@types/validator": "^13.12.1", "browserslist": "^4.23.3", "lightningcss": "^1.26.0", - "openapi-typescript": "^7.3.0", + "openapi-typescript": "^7.4.0", "typescript": "^5.5.4", - "vite": "^5.4.2", + "vite": "^5.4.3", "vite-tsconfig-paths": "^5.0.1" }, "overrides": {