fix: improve sidebar buttons
This commit is contained in:
@@ -1,15 +1,12 @@
|
|||||||
import { op } from '@/utils/op';
|
|
||||||
import { useRouteContext } from '@tanstack/react-router';
|
import { useRouteContext } from '@tanstack/react-router';
|
||||||
import { SparklesIcon } from 'lucide-react';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from './ui/button';
|
import { op } from '@/utils/op';
|
||||||
|
|
||||||
export function FeedbackButton() {
|
export function FeedbackButton({ className }: { className?: string }) {
|
||||||
const context = useRouteContext({ strict: false });
|
const context = useRouteContext({ strict: false });
|
||||||
return (
|
return (
|
||||||
<Button
|
<button
|
||||||
variant={'outline'}
|
className={cn('justify-start text-left text-[13px]', className)}
|
||||||
className="text-left justify-start text-[13px]"
|
|
||||||
icon={SparklesIcon}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
op.track('feedback_button_clicked');
|
op.track('feedback_button_clicked');
|
||||||
if ('uj' in window && window.uj !== undefined) {
|
if ('uj' in window && window.uj !== undefined) {
|
||||||
@@ -23,8 +20,9 @@ export function FeedbackButton() {
|
|||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Give feedback
|
Give feedback
|
||||||
</Button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { CheckIcon, UserIcon } from 'lucide-react';
|
||||||
|
import { themeConfig } from './theme-provider';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -10,11 +11,9 @@ import {
|
|||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
|
||||||
import { CheckIcon, UserIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
import { useLogout } from '@/hooks/use-logout';
|
import { useLogout } from '@/hooks/use-logout';
|
||||||
import { themeConfig } from './theme-provider';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -27,10 +26,13 @@ export function ProfileToggle({ className }: Props) {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="icon" className={className}>
|
<button
|
||||||
|
className={cn(className, 'center-center outline-0')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
<UserIcon className="size-4" />
|
<UserIcon className="size-4" />
|
||||||
<span className="sr-only">Profile</span>
|
<span className="sr-only">Profile</span>
|
||||||
</Button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="center" className="w-56">
|
<DropdownMenuContent align="center" className="w-56">
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
@@ -44,9 +46,9 @@ export function ProfileToggle({ className }: Props) {
|
|||||||
<DropdownMenuSubContent className="p-0">
|
<DropdownMenuSubContent className="p-0">
|
||||||
{themes.map((themeOption) => (
|
{themes.map((themeOption) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
className="capitalize"
|
||||||
key={themeOption.key}
|
key={themeOption.key}
|
||||||
onClick={() => setTheme(themeOption.key)}
|
onClick={() => setTheme(themeOption.key)}
|
||||||
className="capitalize"
|
|
||||||
>
|
>
|
||||||
<span className="mr-2">{themeOption.icon}</span>
|
<span className="mr-2">{themeOption.icon}</span>
|
||||||
{themeOption.label}
|
{themeOption.label}
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
|
import { Link, useNavigate } from '@tanstack/react-router';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
CogIcon,
|
CogIcon,
|
||||||
CreditCardIcon,
|
CreditCardIcon,
|
||||||
LayoutListIcon,
|
LayoutListIcon,
|
||||||
|
PlusIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
WorkflowIcon,
|
WorkflowIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { useAppContext } from '@/hooks/use-app-context';
|
|
||||||
import { useAppParams } from '@/hooks/use-app-params';
|
|
||||||
import { pushModal } from '@/modals';
|
|
||||||
import type { RouterOutputs } from '@/trpc/client';
|
|
||||||
import { cn } from '@/utils/cn';
|
|
||||||
import { Link, useNavigate, useRouteContext } from '@tanstack/react-router';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import { ChevronDownIcon, PlusIcon } from 'lucide-react';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { Badge } from './ui/badge';
|
import { Badge } from './ui/badge';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -23,6 +17,11 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from './ui/dropdown-menu';
|
} from './ui/dropdown-menu';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useAppContext } from '@/hooks/use-app-context';
|
||||||
|
import { pushModal } from '@/modals';
|
||||||
|
import type { RouterOutputs } from '@/trpc/client';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
export default function SidebarOrganizationMenu({
|
export default function SidebarOrganizationMenu({
|
||||||
organization,
|
organization,
|
||||||
@@ -34,35 +33,35 @@ export default function SidebarOrganizationMenu({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 rounded-md px-3 py-2 font-medium transition-all hover:bg-def-200 text-[13px]',
|
|
||||||
)}
|
|
||||||
activeOptions={{ exact: true }}
|
activeOptions={{ exact: true }}
|
||||||
to="/$organizationId"
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-md px-3 py-2 font-medium text-[13px] transition-all hover:bg-def-200'
|
||||||
|
)}
|
||||||
from="/$organizationId"
|
from="/$organizationId"
|
||||||
|
to="/$organizationId"
|
||||||
>
|
>
|
||||||
<LayoutListIcon size={20} />
|
<LayoutListIcon size={20} />
|
||||||
<div className="flex-1">Projects</div>
|
<div className="flex-1">Projects</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 rounded-md px-3 py-2 font-medium transition-all hover:bg-def-200 text-[13px]',
|
|
||||||
)}
|
|
||||||
activeOptions={{ exact: true }}
|
activeOptions={{ exact: true }}
|
||||||
to="/$organizationId/settings"
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-md px-3 py-2 font-medium text-[13px] transition-all hover:bg-def-200'
|
||||||
|
)}
|
||||||
from="/$organizationId"
|
from="/$organizationId"
|
||||||
|
to="/$organizationId/settings"
|
||||||
>
|
>
|
||||||
<CogIcon size={20} />
|
<CogIcon size={20} />
|
||||||
<div className="flex-1">Settings</div>
|
<div className="flex-1">Settings</div>
|
||||||
</Link>
|
</Link>
|
||||||
{!isSelfHosted && (
|
{!isSelfHosted && (
|
||||||
<Link
|
<Link
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 rounded-md px-3 py-2 font-medium transition-all hover:bg-def-200 text-[13px]',
|
|
||||||
)}
|
|
||||||
activeOptions={{ exact: true }}
|
activeOptions={{ exact: true }}
|
||||||
to="/$organizationId/billing"
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-md px-3 py-2 font-medium text-[13px] transition-all hover:bg-def-200'
|
||||||
|
)}
|
||||||
from="/$organizationId"
|
from="/$organizationId"
|
||||||
|
to="/$organizationId/billing"
|
||||||
>
|
>
|
||||||
<CreditCardIcon size={20} />
|
<CreditCardIcon size={20} />
|
||||||
<div className="flex-1">Billing</div>
|
<div className="flex-1">Billing</div>
|
||||||
@@ -74,20 +73,20 @@ export default function SidebarOrganizationMenu({
|
|||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 rounded-md px-3 py-2 font-medium transition-all hover:bg-def-200 text-[13px]',
|
'flex items-center gap-2 rounded-md px-3 py-2 font-medium text-[13px] transition-all hover:bg-def-200'
|
||||||
)}
|
)}
|
||||||
to="/$organizationId/members"
|
|
||||||
from="/$organizationId"
|
from="/$organizationId"
|
||||||
|
to="/$organizationId/members"
|
||||||
>
|
>
|
||||||
<UsersIcon size={20} />
|
<UsersIcon size={20} />
|
||||||
<div className="flex-1">Members</div>
|
<div className="flex-1">Members</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 rounded-md px-3 py-2 font-medium transition-all hover:bg-def-200 text-[13px]',
|
'flex items-center gap-2 rounded-md px-3 py-2 font-medium text-[13px] transition-all hover:bg-def-200'
|
||||||
)}
|
)}
|
||||||
to="/$organizationId/integrations"
|
|
||||||
from="/$organizationId"
|
from="/$organizationId"
|
||||||
|
to="/$organizationId/integrations"
|
||||||
>
|
>
|
||||||
<WorkflowIcon size={20} />
|
<WorkflowIcon size={20} />
|
||||||
<div className="flex-1">Integrations</div>
|
<div className="flex-1">Integrations</div>
|
||||||
@@ -138,20 +137,20 @@ export function ActionCTAButton() {
|
|||||||
<Button className="w-full justify-between" size="default">
|
<Button className="w-full justify-between" size="default">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<PlusIcon size={16} />
|
<PlusIcon size={16} />
|
||||||
<div className="relative h-5 flex items-center">
|
<div className="relative flex h-5 items-center">
|
||||||
<AnimatePresence mode="popLayout">
|
<AnimatePresence mode="popLayout">
|
||||||
<motion.span
|
<motion.span
|
||||||
key={currentActionIndex}
|
|
||||||
initial={{ y: 20, opacity: 0 }}
|
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
className="absolute whitespace-nowrap"
|
||||||
exit={{ y: -20, opacity: 0 }}
|
exit={{ y: -20, opacity: 0 }}
|
||||||
|
initial={{ y: 20, opacity: 0 }}
|
||||||
|
key={currentActionIndex}
|
||||||
transition={{
|
transition={{
|
||||||
type: 'spring',
|
type: 'spring',
|
||||||
stiffness: 300,
|
stiffness: 300,
|
||||||
damping: 25,
|
damping: 25,
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
}}
|
}}
|
||||||
className="absolute whitespace-nowrap"
|
|
||||||
>
|
>
|
||||||
{ACTIONS[currentActionIndex].label}
|
{ACTIONS[currentActionIndex].label}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
@@ -161,12 +160,12 @@ export function ActionCTAButton() {
|
|||||||
<ChevronDownIcon size={16} />
|
<ChevronDownIcon size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="start">
|
<DropdownMenuContent align="start" className="w-56">
|
||||||
{ACTIONS.map((action) => (
|
{ACTIONS.map((action) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={action.onClick}
|
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
key={action.label}
|
key={action.label}
|
||||||
|
onClick={action.onClick}
|
||||||
>
|
>
|
||||||
<action.icon className="mr-2 h-4 w-4" />
|
<action.icon className="mr-2 h-4 w-4" />
|
||||||
{action.label}
|
{action.label}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { pushModal } from '@/modals';
|
|
||||||
import type { IServiceDashboards } from '@openpanel/db';
|
import type { IServiceDashboards } from '@openpanel/db';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
@@ -30,6 +28,8 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from './ui/dropdown-menu';
|
} from './ui/dropdown-menu';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { pushModal } from '@/modals';
|
||||||
|
|
||||||
interface SidebarProjectMenuProps {
|
interface SidebarProjectMenuProps {
|
||||||
dashboards: IServiceDashboards;
|
dashboards: IServiceDashboards;
|
||||||
@@ -40,38 +40,42 @@ export default function SidebarProjectMenu({
|
|||||||
}: SidebarProjectMenuProps) {
|
}: SidebarProjectMenuProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-2 font-medium text-muted-foreground">Analytics</div>
|
<div className="mb-2 font-medium text-muted-foreground text-sm">
|
||||||
<SidebarLink icon={WallpaperIcon} label="Overview" href={'/'} />
|
Analytics
|
||||||
|
</div>
|
||||||
|
<SidebarLink href={'/'} icon={WallpaperIcon} label="Overview" />
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
|
href={'/dashboards'}
|
||||||
icon={LayoutPanelTopIcon}
|
icon={LayoutPanelTopIcon}
|
||||||
label="Dashboards"
|
label="Dashboards"
|
||||||
href={'/dashboards'}
|
|
||||||
/>
|
/>
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
|
href={'/insights'}
|
||||||
icon={TrendingUpDownIcon}
|
icon={TrendingUpDownIcon}
|
||||||
label="Insights"
|
label="Insights"
|
||||||
href={'/insights'}
|
|
||||||
/>
|
/>
|
||||||
<SidebarLink icon={LayersIcon} label="Pages" href={'/pages'} />
|
<SidebarLink href={'/pages'} icon={LayersIcon} label="Pages" />
|
||||||
<SidebarLink icon={Globe2Icon} label="Realtime" href={'/realtime'} />
|
<SidebarLink href={'/realtime'} icon={Globe2Icon} label="Realtime" />
|
||||||
<SidebarLink icon={GanttChartIcon} label="Events" href={'/events'} />
|
<SidebarLink href={'/events'} icon={GanttChartIcon} label="Events" />
|
||||||
<SidebarLink icon={UsersIcon} label="Sessions" href={'/sessions'} />
|
<SidebarLink href={'/sessions'} icon={UsersIcon} label="Sessions" />
|
||||||
<SidebarLink icon={UsersIcon} label="Profiles" href={'/profiles'} />
|
<SidebarLink href={'/profiles'} icon={UsersIcon} label="Profiles" />
|
||||||
<div className="mt-4 mb-2 font-medium text-muted-foreground">Manage</div>
|
<div className="mt-4 mb-2 font-medium text-muted-foreground text-sm">
|
||||||
|
Manage
|
||||||
|
</div>
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
exact={false}
|
exact={false}
|
||||||
|
href={'/settings'}
|
||||||
icon={CogIcon}
|
icon={CogIcon}
|
||||||
label="Settings"
|
label="Settings"
|
||||||
href={'/settings'}
|
|
||||||
/>
|
/>
|
||||||
<SidebarLink icon={GridIcon} label="References" href={'/references'} />
|
<SidebarLink href={'/references'} icon={GridIcon} label="References" />
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
exact={false}
|
exact={false}
|
||||||
|
href={'/notifications'}
|
||||||
icon={BellIcon}
|
icon={BellIcon}
|
||||||
label="Notifications"
|
label="Notifications"
|
||||||
href={'/notifications'}
|
|
||||||
/>
|
/>
|
||||||
<SidebarLink icon={UndoDotIcon} label="Back to workspace" href={'..'} />
|
<SidebarLink href={'..'} icon={UndoDotIcon} label="Back to workspace" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -143,20 +147,20 @@ export function ActionCTAButton() {
|
|||||||
<Button className="w-full justify-between" size="default">
|
<Button className="w-full justify-between" size="default">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<PlusIcon size={16} />
|
<PlusIcon size={16} />
|
||||||
<div className="relative h-5 flex items-center">
|
<div className="relative flex h-5 items-center">
|
||||||
<AnimatePresence mode="popLayout">
|
<AnimatePresence mode="popLayout">
|
||||||
<motion.span
|
<motion.span
|
||||||
key={currentActionIndex}
|
|
||||||
initial={{ y: 20, opacity: 0 }}
|
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
className="absolute whitespace-nowrap"
|
||||||
exit={{ y: -20, opacity: 0 }}
|
exit={{ y: -20, opacity: 0 }}
|
||||||
|
initial={{ y: 20, opacity: 0 }}
|
||||||
|
key={currentActionIndex}
|
||||||
transition={{
|
transition={{
|
||||||
type: 'spring',
|
type: 'spring',
|
||||||
stiffness: 300,
|
stiffness: 300,
|
||||||
damping: 25,
|
damping: 25,
|
||||||
duration: 0.3,
|
duration: 0.3,
|
||||||
}}
|
}}
|
||||||
className="absolute whitespace-nowrap"
|
|
||||||
>
|
>
|
||||||
{ACTIONS[currentActionIndex].label}
|
{ACTIONS[currentActionIndex].label}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
@@ -166,12 +170,12 @@ export function ActionCTAButton() {
|
|||||||
<ChevronDownIcon size={16} />
|
<ChevronDownIcon size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="start">
|
<DropdownMenuContent align="start" className="w-56">
|
||||||
{ACTIONS.map((action) => (
|
{ACTIONS.map((action) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={action.onClick}
|
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
key={action.label}
|
key={action.label}
|
||||||
|
onClick={action.onClick}
|
||||||
>
|
>
|
||||||
<action.icon className="mr-2 h-4 w-4" />
|
<action.icon className="mr-2 h-4 w-4" />
|
||||||
{action.label}
|
{action.label}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { useAppContext } from '@/hooks/use-app-context';
|
|
||||||
import { useTRPC } from '@/integrations/trpc/react';
|
|
||||||
import { cn } from '@/utils/cn';
|
|
||||||
import type { IServiceOrganization } from '@openpanel/db';
|
import type { IServiceOrganization } from '@openpanel/db';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Link, useLocation, useParams } from '@tanstack/react-router';
|
import { Link, useLocation, useParams } from '@tanstack/react-router';
|
||||||
@@ -17,6 +14,9 @@ import SidebarProjectMenu, {
|
|||||||
ActionCTAButton as ActionProjectCTAButton,
|
ActionCTAButton as ActionProjectCTAButton,
|
||||||
} from './sidebar-project-menu';
|
} from './sidebar-project-menu';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
|
import { useAppContext } from '@/hooks/use-app-context';
|
||||||
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const { organizationId, projectId } = useParams({ strict: false });
|
const { organizationId, projectId } = useParams({ strict: false });
|
||||||
@@ -24,7 +24,7 @@ export function Sidebar() {
|
|||||||
|
|
||||||
// Get organizations data
|
// Get organizations data
|
||||||
const { data: organizations = [] } = useQuery(
|
const { data: organizations = [] } = useQuery(
|
||||||
trpc.organization.list.queryOptions(),
|
trpc.organization.list.queryOptions()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get projects data when we have an organization
|
// Get projects data when we have an organization
|
||||||
@@ -86,67 +86,75 @@ export function SidebarContainer({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
onClick={() => setActive(false)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed bottom-0 left-0 right-0 top-0 z-40 backdrop-blur-sm transition-opacity',
|
'fixed top-0 right-0 bottom-0 left-0 z-40 backdrop-blur-sm transition-opacity',
|
||||||
active
|
active
|
||||||
? 'pointer-events-auto opacity-100'
|
? 'pointer-events-auto opacity-100'
|
||||||
: 'pointer-events-none opacity-0',
|
: 'pointer-events-none opacity-0'
|
||||||
)}
|
)}
|
||||||
|
onClick={() => setActive(false)}
|
||||||
|
type="button"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed left-0 top-0 z-40 flex h-screen w-72 flex-col border-r border-border bg-card transition-transform',
|
'fixed top-0 left-0 z-40 flex h-screen w-72 flex-col border-border border-r bg-card transition-transform',
|
||||||
'-translate-x-72 lg:-translate-x-0', // responsive
|
'-translate-x-72 lg:-translate-x-0', // responsive
|
||||||
active && 'translate-x-0', // force active on mobile
|
active && 'translate-x-0' // force active on mobile
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="absolute -right-12 flex h-16 items-center lg:hidden">
|
<div className="absolute -right-12 flex h-16 items-center lg:hidden">
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
|
||||||
onClick={() => setActive((p) => !p)}
|
onClick={() => setActive((p) => !p)}
|
||||||
|
size="icon"
|
||||||
variant={'outline'}
|
variant={'outline'}
|
||||||
>
|
>
|
||||||
{active ? <XIcon size={16} /> : <MenuIcon size={16} />}
|
{active ? <XIcon size={16} /> : <MenuIcon size={16} />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-16 shrink-0 items-center gap-2 border-b border-border px-4">
|
<div className="flex h-16 shrink-0 items-center gap-2 border-border border-b px-4">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<LogoSquare className="max-h-8" />
|
<LogoSquare className="max-h-8" />
|
||||||
</Link>
|
</Link>
|
||||||
<ProjectSelector
|
<ProjectSelector
|
||||||
align="start"
|
align="start"
|
||||||
projects={projects}
|
|
||||||
organizations={organizations}
|
organizations={organizations}
|
||||||
|
projects={projects}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn([
|
className={cn([
|
||||||
'flex flex-grow col gap-1 overflow-auto p-4 hide-scrollbar',
|
'hide-scrollbar flex-1 overflow-auto p-4',
|
||||||
"[&_a[data-status='active']]:bg-def-200",
|
"[&_a[data-status='active']]:bg-def-200",
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
</div>
|
||||||
<div className="mt-auto w-full pt-6">
|
<div className="relative">
|
||||||
<div className="row gap-2 justify-between">
|
<div className="pointer-events-none absolute right-0 bottom-full left-0 h-8 bg-gradient-to-t from-card to-card/0" />
|
||||||
<FeedbackButton />
|
<div className="border-border border-t bg-card">
|
||||||
<ProfileToggle />
|
<div className="flex items-center">
|
||||||
|
<FeedbackButton className="h-12 flex-1 whitespace-nowrap rounded-none border-border border-r px-4 text-muted-foreground outline-0 hover:bg-accent hover:text-accent-foreground" />
|
||||||
|
<a
|
||||||
|
className="flex h-12 flex-1 items-center justify-center gap-2 border-border border-r font-medium text-muted-foreground text-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||||
|
href="https://openpanel.dev/docs"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Docs
|
||||||
|
</a>
|
||||||
|
<ProfileToggle className="h-12 flex-1 rounded-none hover:bg-accent hover:text-accent-foreground" />
|
||||||
</div>
|
</div>
|
||||||
{isSelfHosted && (
|
{isSelfHosted && (
|
||||||
<a
|
<a
|
||||||
|
className="center-center flex h-12 cursor-pointer gap-2 border-border border-t px-4 font-medium text-muted-foreground text-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||||
href="https://openpanel.dev/supporter"
|
href="https://openpanel.dev/supporter"
|
||||||
className="text-center text-sm w-full mt-2 border rounded p-2 font-medium block hover:underline hover:text-primary outline-none"
|
|
||||||
>
|
>
|
||||||
Self-hosted instance, support us!
|
<span>Support Us</span>
|
||||||
|
<span>Pay What You Want</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="fixed bottom-0 left-0 right-0 pointer-events-none">
|
|
||||||
<div className="h-8 w-full bg-gradient-to-t from-card to-card/0" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
import { createFileRoute, Link, redirect } from '@tanstack/react-router';
|
||||||
import FullPageLoadingState from '@/components/full-page-loading-state';
|
import FullPageLoadingState from '@/components/full-page-loading-state';
|
||||||
import { LogoSquare } from '@/components/logo';
|
import { LogoSquare } from '@/components/logo';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
@@ -6,11 +8,9 @@ import { useLogout } from '@/hooks/use-logout';
|
|||||||
import { useNumber } from '@/hooks/use-numer-formatter';
|
import { useNumber } from '@/hooks/use-numer-formatter';
|
||||||
import { useTRPC } from '@/integrations/trpc/react';
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
import { createTitle } from '@/utils/title';
|
import { createTitle } from '@/utils/title';
|
||||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
|
||||||
import { Link, createFileRoute, redirect } from '@tanstack/react-router';
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/')({
|
export const Route = createFileRoute('/')({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: ({ context }) => {
|
||||||
if (!context.session.session) {
|
if (!context.session.session) {
|
||||||
throw redirect({ to: '/login' });
|
throw redirect({ to: '/login' });
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ export const Route = createFileRoute('/')({
|
|||||||
context.trpc.organization.list.queryOptions(undefined, {
|
context.trpc.organization.list.queryOptions(undefined, {
|
||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
|
|
||||||
@@ -50,28 +50,28 @@ function LandingPage() {
|
|||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
const logout = useLogout();
|
const logout = useLogout();
|
||||||
const { data: organizations } = useSuspenseQuery(
|
const { data: organizations } = useSuspenseQuery(
|
||||||
trpc.organization.list.queryOptions(),
|
trpc.organization.list.queryOptions()
|
||||||
);
|
);
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col items-center justify-center">
|
<div className="flex min-h-screen flex-col items-center justify-center">
|
||||||
<div className="max-w-2xl mx-auto px-4 col gap-12">
|
<div className="col mx-auto max-w-2xl gap-12 px-4 py-8">
|
||||||
<div className="col gap-4">
|
<div className="col gap-4">
|
||||||
<LogoSquare className="w-full max-w-24" />
|
<LogoSquare className="w-full max-w-14 md:max-w-24" />
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Welcome to OpenPanel.dev"
|
|
||||||
description="The best web and product analytics tool out there (our honest opinion)."
|
description="The best web and product analytics tool out there (our honest opinion)."
|
||||||
|
title="Welcome to OpenPanel.dev"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col gap-2">
|
<div className="col gap-2">
|
||||||
{organizations?.map((org) => (
|
{organizations?.map((org) => (
|
||||||
<Link
|
<Link
|
||||||
|
className="row items-center justify-between rounded-lg border bg-card p-3 transition-all hover:translate-x-1 hover:border-primary hover:shadow-md"
|
||||||
key={org.id}
|
key={org.id}
|
||||||
to={'/$organizationId'}
|
|
||||||
params={{ organizationId: org.id }}
|
params={{ organizationId: org.id }}
|
||||||
className="row justify-between items-center p-3 rounded-lg border bg-card hover:border-primary hover:shadow-md transition-all hover:translate-x-1"
|
to={'/$organizationId'}
|
||||||
>
|
>
|
||||||
<div className="col gap-2">
|
<div className="col gap-2">
|
||||||
<span className="font-medium text-lg">{org.name}</span>
|
<span className="font-medium text-lg">{org.name}</span>
|
||||||
@@ -89,7 +89,7 @@ function LandingPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="row gap-4">
|
<div className="row gap-4">
|
||||||
<Button onClick={() => logout.mutate()} loading={logout.isPending}>
|
<Button loading={logout.isPending} onClick={() => logout.mutate()}>
|
||||||
Log out
|
Log out
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user