Fixed share page. Updated device detect. Updated teams fetch.

This commit is contained in:
Mike Cao 2025-09-25 11:21:44 -07:00
parent 27c342811e
commit 1b400da7b2
16 changed files with 118 additions and 109 deletions

View file

@ -82,7 +82,7 @@
"@react-spring/web": "^10.0.1", "@react-spring/web": "^10.0.1",
"@svgr/cli": "^8.1.0", "@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.85.5", "@tanstack/react-query": "^5.85.5",
"@umami/react-zen": "^0.184.0", "@umami/react-zen": "^0.186.0",
"@umami/redis-client": "^0.29.0", "@umami/redis-client": "^0.29.0",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"chalk": "^5.6.0", "chalk": "^5.6.0",
@ -130,6 +130,7 @@
"semver": "^7.5.4", "semver": "^7.5.4",
"serialize-error": "^12.0.0", "serialize-error": "^12.0.0",
"thenby": "^1.3.4", "thenby": "^1.3.4",
"ua-parser-js": "^2.0.5",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"zod": "^4.1.5", "zod": "^4.1.5",
"zustand": "^5.0.8" "zustand": "^5.0.8"

47
pnpm-lock.yaml generated
View file

@ -45,8 +45,8 @@ importers:
specifier: ^5.85.5 specifier: ^5.85.5
version: 5.85.5(react@19.1.1) version: 5.85.5(react@19.1.1)
'@umami/react-zen': '@umami/react-zen':
specifier: ^0.184.0 specifier: ^0.186.0
version: 0.184.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) version: 0.186.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))
'@umami/redis-client': '@umami/redis-client':
specifier: ^0.29.0 specifier: ^0.29.0
version: 0.29.0 version: 0.29.0
@ -188,6 +188,9 @@ importers:
thenby: thenby:
specifier: ^1.3.4 specifier: ^1.3.4
version: 1.3.4 version: 1.3.4
ua-parser-js:
specifier: ^2.0.5
version: 2.0.5
uuid: uuid:
specifier: ^11.1.0 specifier: ^11.1.0
version: 11.1.0 version: 11.1.0
@ -364,6 +367,8 @@ importers:
specifier: ^5.9.2 specifier: ^5.9.2
version: 5.9.2 version: 5.9.2
dist: {}
packages: packages:
'@ampproject/remapping@2.3.0': '@ampproject/remapping@2.3.0':
@ -2733,8 +2738,8 @@ packages:
'@prisma/client': ^6.1.0 '@prisma/client': ^6.1.0
'@prisma/extension-read-replicas': ^0.4.1 '@prisma/extension-read-replicas': ^0.4.1
'@umami/react-zen@0.184.0': '@umami/react-zen@0.186.0':
resolution: {integrity: sha512-XfxTiP4ljumflx02ymDMXLnhcJW+mOxxKCPEVEjuDrQfR6VUlbHg0EdH04S4gvCJZJC/WnP6guyO2eabhJL88Q==} resolution: {integrity: sha512-s+x4cJK5UTHQ0l2TTUb3zX8P2U6bMw35NRjIqG+OJvljJf5NNdRo6WChZOvnh/08XxGI30jntFhUYdup255rFg==}
'@umami/redis-client@0.29.0': '@umami/redis-client@0.29.0':
resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==} resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==}
@ -3695,6 +3700,9 @@ packages:
detect-browser@5.3.0: detect-browser@5.3.0:
resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==} resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==}
detect-europe-js@0.1.2:
resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==}
detect-indent@6.1.0: detect-indent@6.1.0:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -4719,6 +4727,9 @@ packages:
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
is-standalone-pwa@0.1.1:
resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==}
is-stream@2.0.1: is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -7202,6 +7213,13 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
ua-is-frozen@0.1.2:
resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==}
ua-parser-js@2.0.5:
resolution: {integrity: sha512-sZErtx3rhpvZQanWW5umau4o/snfoLqRcQwQIZ54377WtRzIecnIKvjpkd5JwPcSUMglGnbIgcsQBGAbdi3S9Q==}
hasBin: true
ufo@1.6.1: ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
@ -7217,6 +7235,10 @@ packages:
undici-types@7.10.0: undici-types@7.10.0:
resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
undici@7.16.0:
resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==}
engines: {node: '>=20.18.1'}
unicorn-magic@0.3.0: unicorn-magic@0.3.0:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -10321,7 +10343,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@umami/react-zen@0.184.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))': '@umami/react-zen@0.186.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))':
dependencies: dependencies:
'@fontsource/jetbrains-mono': 5.2.8 '@fontsource/jetbrains-mono': 5.2.8
'@internationalized/date': 3.9.0 '@internationalized/date': 3.9.0
@ -11398,6 +11420,8 @@ snapshots:
detect-browser@5.3.0: {} detect-browser@5.3.0: {}
detect-europe-js@0.1.2: {}
detect-indent@6.1.0: {} detect-indent@6.1.0: {}
detect-libc@2.0.4: detect-libc@2.0.4:
@ -12606,6 +12630,8 @@ snapshots:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
is-standalone-pwa@0.1.1: {}
is-stream@2.0.1: {} is-stream@2.0.1: {}
is-string@1.1.1: is-string@1.1.1:
@ -15546,6 +15572,15 @@ snapshots:
typescript@5.9.2: {} typescript@5.9.2: {}
ua-is-frozen@0.1.2: {}
ua-parser-js@2.0.5:
dependencies:
detect-europe-js: 0.1.2
is-standalone-pwa: 0.1.1
ua-is-frozen: 0.1.2
undici: 7.16.0
ufo@1.6.1: {} ufo@1.6.1: {}
uglify-js@3.19.3: uglify-js@3.19.3:
@ -15560,6 +15595,8 @@ snapshots:
undici-types@7.10.0: {} undici-types@7.10.0: {}
undici@7.16.0: {}
unicorn-magic@0.3.0: {} unicorn-magic@0.3.0: {}
universalify@0.1.2: {} universalify@0.1.2: {}

View file

@ -10,7 +10,7 @@ import {
} from '@umami/react-zen'; } from '@umami/react-zen';
import { Globe, LinkIcon, LogoSvg, Grid2x2, PanelLeft } from '@/components/icons'; import { Globe, LinkIcon, LogoSvg, Grid2x2, PanelLeft } from '@/components/icons';
import { useMessages, useNavigation, useGlobalState } from '@/components/hooks'; import { useMessages, useNavigation, useGlobalState } from '@/components/hooks';
import { TeamsButton } from '@/components/input/TeamsButton'; import { NavButton } from '@/components/input/NavButton';
import { PanelButton } from '@/components/input/PanelButton'; import { PanelButton } from '@/components/input/PanelButton';
import { Key } from 'react'; import { Key } from 'react';
import { SettingsButton } from '@/components/input/SettingsButton'; import { SettingsButton } from '@/components/input/SettingsButton';
@ -48,8 +48,8 @@ export function SideNav(props: SidebarProps) {
}; };
return ( return (
<Row height="100%" backgroundColor border="right"> <Row height="100%" backgroundColor>
<Sidebar {...props} isCollapsed={isCollapsed || hasNav} muteItems={false} showBorder={false}> <Sidebar {...props} isCollapsed={isCollapsed || hasNav}>
<SidebarSection onClick={() => setIsCollapsed(false)}> <SidebarSection onClick={() => setIsCollapsed(false)}>
<SidebarHeader <SidebarHeader
label="umami" label="umami"
@ -60,7 +60,7 @@ export function SideNav(props: SidebarProps) {
</SidebarHeader> </SidebarHeader>
</SidebarSection> </SidebarSection>
<SidebarSection paddingTop="0" paddingBottom="0" justifyContent="center"> <SidebarSection paddingTop="0" paddingBottom="0" justifyContent="center">
<TeamsButton showText={!hasNav && !isCollapsed} onAction={handleSelect} /> <NavButton showText={!hasNav && !isCollapsed} onAction={handleSelect} />
</SidebarSection> </SidebarSection>
<SidebarSection flexGrow={1}> <SidebarSection flexGrow={1}>
{links.map(({ id, path, label, icon }) => { {links.map(({ id, path, label, icon }) => {

View file

@ -53,7 +53,6 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
title={formatMessage(labels.settings)} title={formatMessage(labels.settings)}
selectedKey={selectedKey} selectedKey={selectedKey}
allowMinimize={false} allowMinimize={false}
muteItems={false}
/> />
</Column> </Column>
<Column gap="6" margin="2"> <Column gap="6" margin="2">

View file

@ -182,7 +182,7 @@ export function WebsiteExpandedView({
return ( return (
<Grid columns="auto 1fr" gap="6" height="100%" overflow="hidden"> <Grid columns="auto 1fr" gap="6" height="100%" overflow="hidden">
<Column gap="6" border="right" paddingRight="3" overflowY="auto"> <Column gap="6" border="right" paddingRight="3" overflowY="auto">
<SideMenu items={items} selectedKey={view} muteItems={false} /> <SideMenu items={items} selectedKey={view} />
</Column> </Column>
<Column overflow="hidden"> <Column overflow="hidden">
<MetricsExpandedTable <MetricsExpandedTable

View file

@ -161,7 +161,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
.find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id; .find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
return ( return (
<SideMenu items={items} selectedKey={selectedKey} allowMinimize={false} muteItems={false}> <SideMenu items={items} selectedKey={selectedKey} allowMinimize={false}>
<WebsiteSelect <WebsiteSelect
websiteId={websiteId} websiteId={websiteId}
teamId={teamId} teamId={teamId}

View file

@ -1,5 +1,6 @@
import { parseRequest } from '@/lib/request'; import { parseRequest } from '@/lib/request';
import { json } from '@/lib/response'; import { json } from '@/lib/response';
import { getAllUserTeams } from '@/queries';
export async function POST(request: Request) { export async function POST(request: Request) {
const { auth, error } = await parseRequest(request); const { auth, error } = await parseRequest(request);
@ -8,5 +9,7 @@ export async function POST(request: Request) {
return error(); return error();
} }
return json(auth.user); const teams = await getAllUserTeams(auth.user.id);
return json({ ...auth.user, teams });
} }

View file

@ -3,7 +3,7 @@ import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
export function Footer() { export function Footer() {
return ( return (
<Row as="footer"> <Row as="footer" paddingY="6" justifyContent="flex-end">
<a href={HOMEPAGE_URL} target="_blank"> <a href={HOMEPAGE_URL} target="_blank">
<Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`} <Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`}
</a> </a>

View file

@ -1,20 +1,19 @@
import { Row, Icon, Text, ThemeButton } from '@umami/react-zen'; import { Row, Icon, Text, ThemeButton } from '@umami/react-zen';
import Link from 'next/link';
import { LanguageButton } from '@/components/input/LanguageButton'; import { LanguageButton } from '@/components/input/LanguageButton';
import { PreferencesButton } from '@/components/input/PreferencesButton'; import { PreferencesButton } from '@/components/input/PreferencesButton';
import { Logo } from '@/components/icons'; import { LogoSvg } from '@/components/icons';
export function Header() { export function Header() {
return ( return (
<Row as="header"> <Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
<Row gap> <a href="https://umami.is" target="_blank">
<Link href="https://umami.is" target="_blank"> <Row alignItems="center" gap>
<Icon size="lg"> <Icon>
<Logo /> <LogoSvg />
</Icon> </Icon>
<Text>umami</Text> <Text weight="bold">umami</Text>
</Link> </Row>
</Row> </a>
<Row alignItems="center" gap> <Row alignItems="center" gap>
<ThemeButton /> <ThemeButton />
<LanguageButton /> <LanguageButton />

View file

@ -1,4 +1,5 @@
'use client'; 'use client';
import { Column } from '@umami/react-zen';
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage'; import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
import { useShareTokenQuery } from '@/components/hooks'; import { useShareTokenQuery } from '@/components/hooks';
@ -14,12 +15,14 @@ export function SharePage({ shareId }) {
} }
return ( return (
<PageBody> <Column backgroundColor="2">
<Header /> <PageBody gap>
<WebsiteProvider websiteId={shareToken.websiteId}> <Header />
<WebsitePage websiteId={shareToken.websiteId} /> <WebsiteProvider websiteId={shareToken.websiteId}>
</WebsiteProvider> <WebsitePage websiteId={shareToken.websiteId} />
<Footer /> </WebsiteProvider>
</PageBody> <Footer />
</PageBody>
</Column>
); );
} }

View file

@ -10,9 +10,8 @@ import {
Row, Row,
Column, Column,
Pressable, Pressable,
Loading,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks'; import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
import { ChevronRight, User, Users } from '@/components/icons'; import { ChevronRight, User, Users } from '@/components/icons';
export interface TeamsButtonProps { export interface TeamsButtonProps {
@ -20,19 +19,14 @@ export interface TeamsButtonProps {
onAction?: (id: any) => void; onAction?: (id: any) => void;
} }
export function TeamsButton({ showText = true, onAction }: TeamsButtonProps) { export function NavButton({ showText = true, onAction }: TeamsButtonProps) {
const { user } = useLoginQuery(); const { user } = useLoginQuery();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { data, isLoading } = useUserTeamsQuery(user.id);
const { teamId } = useNavigation(); const { teamId } = useNavigation();
const team = data?.data?.find(({ id }) => id === teamId); const team = user?.teams?.find(({ id }) => id === teamId);
const selectedKeys = new Set([teamId || 'user']); const selectedKeys = new Set([teamId || 'user']);
const label = teamId ? team?.name : user.username; const label = teamId ? team?.name : user.username;
if (isLoading) {
return <Loading icon="dots" size="sm" placement="center" />;
}
return ( return (
<MenuTrigger> <MenuTrigger>
<Pressable> <Pressable>
@ -41,10 +35,13 @@ export function TeamsButton({ showText = true, onAction }: TeamsButtonProps) {
justifyContent="space-between" justifyContent="space-between"
flexGrow={1} flexGrow={1}
padding padding
backgroundColor="2" border
borderRadius
shadow="1"
maxHeight="40px"
style={{ cursor: 'pointer', textWrap: 'nowrap', outline: 'none' }} style={{ cursor: 'pointer', textWrap: 'nowrap', outline: 'none' }}
> >
<Row alignItems="center" gap> <Row alignItems="center" position="relative" gap maxHeight="40px">
<Icon>{teamId ? <Users /> : <User />}</Icon> <Icon>{teamId ? <Users /> : <User />}</Icon>
{showText && <Text>{label}</Text>} {showText && <Text>{label}</Text>}
</Row> </Row>
@ -75,7 +72,7 @@ export function TeamsButton({ showText = true, onAction }: TeamsButtonProps) {
</MenuSection> </MenuSection>
<MenuSeparator /> <MenuSeparator />
<MenuSection title={formatMessage(labels.teams)}> <MenuSection title={formatMessage(labels.teams)}>
{data?.data?.map(({ id, name }) => ( {user?.teams?.map(({ id, name }) => (
<MenuItem key={id} id={id}> <MenuItem key={id} id={id}>
<Row alignItems="center" gap> <Row alignItems="center" gap>
<Icon size="sm"> <Icon size="sm">

View file

@ -3,6 +3,7 @@ import { TimezoneSetting } from '@/app/(main)/settings/preferences/TimezoneSetti
import { DateRangeSetting } from '@/app/(main)/settings/preferences/DateRangeSetting'; import { DateRangeSetting } from '@/app/(main)/settings/preferences/DateRangeSetting';
import { Settings } from '@/components/icons'; import { Settings } from '@/components/icons';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { Panel } from '@/components/common/Panel';
export function PreferencesButton() { export function PreferencesButton() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -15,12 +16,16 @@ export function PreferencesButton() {
</Icon> </Icon>
</Button> </Button>
<Popover placement="bottom end"> <Popover placement="bottom end">
<Column gap="3"> <Panel gap="3">
<Label>{formatMessage(labels.timezone)}</Label> <Column>
<TimezoneSetting /> <Label>{formatMessage(labels.timezone)}</Label>
<Label>{formatMessage(labels.defaultDateRange)}</Label> <TimezoneSetting />
<DateRangeSetting /> </Column>
</Column> <Column>
<Label>{formatMessage(labels.defaultDateRange)}</Label>
<DateRangeSetting />
</Column>
</Panel>
</Popover> </Popover>
</DialogTrigger> </DialogTrigger>
); );

View file

@ -74,7 +74,7 @@ export * from '@/components/input/DateFilter';
export * from '@/components/input/DownloadButton'; export * from '@/components/input/DownloadButton';
export * from '@/components/input/ExportButton'; export * from '@/components/input/ExportButton';
export * from '@/components/input/FilterButtons'; export * from '@/components/input/FilterButtons';
export * from '@/components/input/TeamsButton'; export * from '@/components/input/NavButton';
export * from '@/components/input/ProfileButton'; export * from '@/components/input/ProfileButton';
export * from '@/components/input/WebsiteSelect'; export * from '@/components/input/WebsiteSelect';

View file

@ -1,4 +1,5 @@
import path from 'path'; import path from 'node:path';
import { UAParser } from 'ua-parser-js';
import { browserName, detectOS } from 'detect-browser'; import { browserName, detectOS } from 'detect-browser';
import isLocalhost from 'is-localhost-ip'; import isLocalhost from 'is-localhost-ip';
import ipaddr from 'ipaddr.js'; import ipaddr from 'ipaddr.js';
@ -7,35 +8,6 @@ import { safeDecodeURIComponent } from '@/lib/url';
const MAXMIND = 'maxmind'; const MAXMIND = 'maxmind';
export const DESKTOP_OS = [
'BeOS',
'Chrome OS',
'Linux',
'Mac OS',
'Open BSD',
'OS/2',
'QNX',
'Sun OS',
'Windows 10',
'Windows 2000',
'Windows 3.11',
'Windows 7',
'Windows 8',
'Windows 8.1',
'Windows 95',
'Windows 98',
'Windows ME',
'Windows Server 2003',
'Windows Vista',
'Windows XP',
];
export const MOBILE_OS = ['Amazon OS', 'Android OS', 'BlackBerry OS', 'iOS', 'Windows Mobile'];
export const DESKTOP_SCREEN_WIDTH = 1920;
export const LAPTOP_SCREEN_WIDTH = 1024;
export const MOBILE_SCREEN_WIDTH = 479;
// The order here is important and influences how IPs are detected by lib/detect.ts // The order here is important and influences how IPs are detected by lib/detect.ts
// Please do not change the order unless you know exactly what you're doing - read https://developers.cloudflare.com/fundamentals/reference/http-headers/ // Please do not change the order unless you know exactly what you're doing - read https://developers.cloudflare.com/fundamentals/reference/http-headers/
export const IP_ADDRESS_HEADERS = [ export const IP_ADDRESS_HEADERS = [
@ -121,32 +93,10 @@ export function getIpAddress(headers: Headers) {
return ip; return ip;
} }
export function getDevice(screen: string, os: string) { export function getDevice(userAgent: string) {
if (!screen) return; const { device } = UAParser(userAgent);
const [width] = screen.split('x'); return device?.type || 'desktop';
if (DESKTOP_OS.includes(os)) {
if (os === 'Chrome OS' || +width < DESKTOP_SCREEN_WIDTH) {
return 'laptop';
}
return 'desktop';
} else if (MOBILE_OS.includes(os)) {
if (os === 'Amazon OS' || +width > MOBILE_SCREEN_WIDTH) {
return 'tablet';
}
return 'mobile';
}
if (+width >= DESKTOP_SCREEN_WIDTH) {
return 'desktop';
} else if (+width >= LAPTOP_SCREEN_WIDTH) {
return 'laptop';
} else if (+width >= MOBILE_SCREEN_WIDTH) {
return 'tablet';
} else {
return 'mobile';
}
} }
function getRegionCode(country: string, region: string) { function getRegionCode(country: string, region: string) {
@ -221,7 +171,7 @@ export async function getClientInfo(request: Request, payload: Record<string, an
const city = safeDecodeURIComponent(location?.city); const city = safeDecodeURIComponent(location?.city);
const browser = browserName(userAgent); const browser = browserName(userAgent);
const os = detectOS(userAgent) as string; const os = detectOS(userAgent) as string;
const device = getDevice(payload?.screen, os); const device = getDevice(userAgent);
return { userAgent, browser, os, ip, country, region, city, device }; return { userAgent, browser, os, ip, country, region, city, device };
} }

View file

@ -42,7 +42,7 @@ export async function getTeams(
); );
} }
export async function getUserTeams(userId: string, filters: QueryFilters) { export async function getUserTeams(userId: string, filters: QueryFilters = {}) {
return getTeams( return getTeams(
{ {
where: { where: {
@ -80,6 +80,22 @@ export async function getUserTeams(userId: string, filters: QueryFilters) {
); );
} }
export async function getAllUserTeams(userId: string) {
return prisma.client.team.findMany({
where: {
deletedAt: null,
members: {
some: { userId },
},
},
select: {
id: true,
name: true,
logoUrl: true,
},
});
}
export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise<any> { export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise<any> {
const { id } = data; const { id } = data;
const { client, transaction } = prisma; const { client, transaction } = prisma;

View file

@ -6,7 +6,6 @@ body {
background-color: var(--background-color); background-color: var(--background-color);
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
overflow: hidden;
} }
html[style*='padding-right'] { html[style*='padding-right'] {