fix(dashboard): pagination and login
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { FeatureCardBackground } from '@/components/feature-card';
|
||||
import { Section, SectionHeader, SectionLabel } from '@/components/section';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { QuoteIcon } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import Markdown from 'react-markdown';
|
||||
import { FeatureCardBackground } from '@/components/feature-card';
|
||||
import { Section, SectionHeader, SectionLabel } from '@/components/section';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const images = [
|
||||
{
|
||||
@@ -65,55 +63,54 @@ const quotes: {
|
||||
];
|
||||
|
||||
export function WhyOpenPanel() {
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
return (
|
||||
<Section className="container gap-16">
|
||||
<SectionHeader label="Trusted by founders" title="Who uses OpenPanel?" />
|
||||
<div className="col overflow-hidden">
|
||||
<SectionLabel className="text-muted-foreground bg-background -mb-2 z-5 self-start pr-4">
|
||||
<SectionLabel className="z-5 -mb-2 self-start bg-background pr-4 text-muted-foreground">
|
||||
USED BY
|
||||
</SectionLabel>
|
||||
<div className="grid grid-cols-3 md:grid-cols-6 -mx-4 border-y py-4">
|
||||
<div className="-mx-4 grid grid-cols-3 border-y py-4 md:grid-cols-6">
|
||||
{images.map((image) => (
|
||||
<div key={image.logo} className="px-4 border-r last:border-r-0 ">
|
||||
<div className="border-r px-4 last:border-r-0" key={image.logo}>
|
||||
<a
|
||||
className={cn('group center-center relative aspect-square')}
|
||||
href={image.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
key={image.logo}
|
||||
className={cn('relative group center-center aspect-square')}
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
title={image.name}
|
||||
>
|
||||
<FeatureCardBackground />
|
||||
<Image
|
||||
src={image.logo}
|
||||
alt={image.name}
|
||||
width={64}
|
||||
height={64}
|
||||
className={cn('size-16 object-contain dark:invert')}
|
||||
height={64}
|
||||
src={image.logo}
|
||||
width={64}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 -mx-4 border-y py-4">
|
||||
{quotes.slice(0, showMore ? quotes.length : 2).map((quote) => (
|
||||
<div className="-mx-4 grid grid-cols-1 border-y py-4 md:grid-cols-2">
|
||||
{quotes.map((quote) => (
|
||||
<figure
|
||||
className="group px-4 py-4 md:odd:border-r"
|
||||
key={quote.author}
|
||||
className="px-4 py-4 md:odd:border-r group"
|
||||
>
|
||||
<QuoteIcon className="size-10 text-muted-foreground/50 stroke-1 mb-2 group-hover:text-foreground group-hover:rotate-6 transition-all" />
|
||||
<blockquote className="text-xl prose">
|
||||
<QuoteIcon className="mb-2 size-10 stroke-1 text-muted-foreground/50 transition-all group-hover:rotate-6 group-hover:text-foreground" />
|
||||
<blockquote className="prose text-xl">
|
||||
<Markdown>{quote.quote}</Markdown>
|
||||
</blockquote>
|
||||
<figcaption className="row justify-between text-muted-foreground text-sm mt-4">
|
||||
<figcaption className="row mt-4 justify-between text-muted-foreground text-sm">
|
||||
<span>{quote.author}</span>
|
||||
{quote.site && (
|
||||
<cite className="not-italic">
|
||||
<a
|
||||
href={quote.site}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{quote.site.replace('https://', '')}
|
||||
</a>
|
||||
@@ -123,14 +120,6 @@ export function WhyOpenPanel() {
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setShowMore((p) => !p)}
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="self-end mt-4"
|
||||
>
|
||||
{showMore ? 'Show less' : 'View more reviews'}
|
||||
</Button>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useAppContext } from '@/hooks/use-app-context';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { MenuIcon, XIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { LogoSquare } from './logo';
|
||||
import { Button, LinkButton } from './ui/button';
|
||||
import { Button } from './ui/button';
|
||||
import { useAppContext } from '@/hooks/use-app-context';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
export function LoginNavbar({ className }: { className?: string }) {
|
||||
const { isSelfHosted } = useAppContext();
|
||||
@@ -12,59 +12,61 @@ export function LoginNavbar({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-0 left-0 w-full row justify-between items-center p-8 z-10',
|
||||
className,
|
||||
'row absolute top-0 left-0 z-10 w-full items-center justify-between p-8',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<a href="https://openpanel.dev" className="row items-center gap-2">
|
||||
<a className="row items-center gap-2" href="https://openpanel.dev">
|
||||
<LogoSquare className="size-8 shrink-0" />
|
||||
<span className="font-medium text-sm text-muted-foreground">
|
||||
<span className="font-medium text-muted-foreground text-sm">
|
||||
{isSelfHosted ? 'Self-hosted analytics' : 'OpenPanel.dev'}
|
||||
</span>
|
||||
</a>
|
||||
<nav className="max-md:hidden">
|
||||
<ul className="row gap-4 items-center [&>li>a]:text-sm [&>li>a]:text-muted-foreground [&>li>a]:hover:underline">
|
||||
<li>
|
||||
<a href="https://openpanel.dev">OpenPanel Cloud</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://openpanel.dev/compare/mixpanel-alternative">
|
||||
Mixpanel alternative
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://openpanel.dev/compare/posthog-alternative">
|
||||
Posthog alternative
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://openpanel.dev/articles/open-source-web-analytics">
|
||||
Open source analytics
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="md:hidden relative">
|
||||
{isSelfHosted && (
|
||||
<nav className="max-md:hidden">
|
||||
<ul className="row items-center gap-4 [&>li>a]:text-muted-foreground [&>li>a]:text-sm [&>li>a]:hover:underline">
|
||||
<li>
|
||||
<a href="https://openpanel.dev">OpenPanel Cloud</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://openpanel.dev/compare/mixpanel-alternative">
|
||||
Mixpanel alternative
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://openpanel.dev/compare/posthog-alternative">
|
||||
Posthog alternative
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://openpanel.dev/articles/open-source-web-analytics">
|
||||
Open source analytics
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
<div className="relative md:hidden">
|
||||
<Button
|
||||
onClick={() => setMobileMenuOpen((prev) => !prev)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => setMobileMenuOpen((prev) => !prev)}
|
||||
>
|
||||
{mobileMenuOpen ? <XIcon size={20} /> : <MenuIcon size={20} />}
|
||||
</Button>
|
||||
{mobileMenuOpen && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
type="button"
|
||||
/>
|
||||
<nav className="absolute right-0 top-full mt-2 z-50 bg-card border border-border rounded-md shadow-lg min-w-48 py-2">
|
||||
<ul className="flex flex-col *:text-sm *:text-muted-foreground">
|
||||
<nav className="absolute top-full right-0 z-50 mt-2 min-w-48 rounded-md border border-border bg-card py-2 shadow-lg">
|
||||
<ul className="flex flex-col *:text-muted-foreground *:text-sm">
|
||||
<li>
|
||||
<a
|
||||
href="https://openpanel.dev"
|
||||
className="block px-4 py-2 hover:bg-accent hover:text-accent-foreground"
|
||||
href="https://openpanel.dev"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
OpenPanel Cloud
|
||||
@@ -72,8 +74,8 @@ export function LoginNavbar({ className }: { className?: string }) {
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://openpanel.dev/compare/mixpanel-alternative"
|
||||
className="block px-4 py-2 hover:bg-accent hover:text-accent-foreground"
|
||||
href="https://openpanel.dev/compare/mixpanel-alternative"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Posthog alternative
|
||||
@@ -81,8 +83,8 @@ export function LoginNavbar({ className }: { className?: string }) {
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://openpanel.dev/compare/mixpanel-alternative"
|
||||
className="block px-4 py-2 hover:bg-accent hover:text-accent-foreground"
|
||||
href="https://openpanel.dev/compare/mixpanel-alternative"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Mixpanel alternative
|
||||
@@ -90,8 +92,8 @@ export function LoginNavbar({ className }: { className?: string }) {
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://openpanel.dev/articles/open-source-web-analytics"
|
||||
className="block px-4 py-2 hover:bg-accent hover:text-accent-foreground"
|
||||
href="https://openpanel.dev/articles/open-source-web-analytics"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Open source analytics
|
||||
|
||||
@@ -1,119 +1,102 @@
|
||||
import { LogoSquare } from '@/components/logo';
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from '@/components/ui/carousel';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { CodeIcon, CreditCardIcon, DollarSignIcon } from 'lucide-react';
|
||||
import { SellingPoint } from './selling-points';
|
||||
import Autoplay from 'embla-carousel-autoplay';
|
||||
import { QuoteIcon } from 'lucide-react';
|
||||
|
||||
const onboardingSellingPoints = [
|
||||
const testimonials = [
|
||||
{
|
||||
key: 'get-started',
|
||||
render: () => (
|
||||
<SellingPoint
|
||||
bgImage="/img-6.webp"
|
||||
title="Get started in minutes"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
<DollarSignIcon className="size-4 inline-block mr-1 relative -top-0.5" />
|
||||
Free trial
|
||||
</p>
|
||||
<p>
|
||||
<CreditCardIcon className="size-4 inline-block mr-1 relative -top-0.5" />
|
||||
No credit card required
|
||||
</p>
|
||||
<p>
|
||||
<CodeIcon className="size-4 inline-block mr-1 relative -top-0.5" />
|
||||
Add our tracking code and get insights in real-time.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
),
|
||||
key: 'thomas',
|
||||
bgImage: '/img-1.webp',
|
||||
quote:
|
||||
"OpenPanel is BY FAR the best open-source analytics I've ever seen. Better UX/UI, many more features, and incredible support from the founder.",
|
||||
author: 'Thomas Sanlis',
|
||||
site: 'uneed.best',
|
||||
},
|
||||
{
|
||||
key: 'welcome',
|
||||
render: () => (
|
||||
<SellingPoint
|
||||
bgImage="/img-1.webp"
|
||||
title="Best open-source alternative"
|
||||
description="Mixpanel too expensive, Google Analytics has no privacy, Amplitude old and boring"
|
||||
/>
|
||||
),
|
||||
key: 'julien',
|
||||
bgImage: '/img-2.webp',
|
||||
quote:
|
||||
'After testing several product analytics tools, we chose OpenPanel and we are very satisfied. Profiles and Conversion Events are our favorite features.',
|
||||
author: 'Julien Hany',
|
||||
site: 'strackr.com',
|
||||
},
|
||||
{
|
||||
key: 'selling-point-2',
|
||||
render: () => (
|
||||
<SellingPoint
|
||||
bgImage="/img-2.webp"
|
||||
title="Fast and reliable"
|
||||
description="Never miss a beat with our real-time analytics"
|
||||
/>
|
||||
),
|
||||
key: 'piotr',
|
||||
bgImage: '/img-3.webp',
|
||||
quote:
|
||||
'The Overview tab is great — it has everything I need. The UI is beautiful, clean, modern, very pleasing to the eye.',
|
||||
author: 'Piotr Kulpinski',
|
||||
site: 'producthunt.com',
|
||||
},
|
||||
{
|
||||
key: 'selling-point-3',
|
||||
render: () => (
|
||||
<SellingPoint
|
||||
bgImage="/img-3.webp"
|
||||
title="Easy to use"
|
||||
description="Compared to other tools we have kept it simple"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'selling-point-4',
|
||||
render: () => (
|
||||
<SellingPoint
|
||||
bgImage="/img-4.webp"
|
||||
title="Privacy by default"
|
||||
description="We have built our platform with privacy at its heart"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'selling-point-5',
|
||||
render: () => (
|
||||
<SellingPoint
|
||||
bgImage="/img-5.webp"
|
||||
title="Open source"
|
||||
description="You can inspect the code and self-host if you choose"
|
||||
/>
|
||||
),
|
||||
key: 'selfhost',
|
||||
bgImage: '/img-4.webp',
|
||||
quote:
|
||||
"After paying a lot to PostHog for years, OpenPanel gives us the same — in many ways better — analytics while keeping full ownership of our data. We don't want to run any business without OpenPanel anymore.",
|
||||
author: 'Self-hosting user',
|
||||
site: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
function TestimonialSlide({
|
||||
bgImage,
|
||||
quote,
|
||||
author,
|
||||
site,
|
||||
}: {
|
||||
bgImage: string;
|
||||
quote: string;
|
||||
author: string;
|
||||
site?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex flex-col justify-end h-full p-10 select-none">
|
||||
<img
|
||||
src={bgImage}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
alt=""
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-black/10" />
|
||||
<div className="relative z-10 flex flex-col gap-4">
|
||||
<QuoteIcon className="size-10 text-white/40 stroke-1" />
|
||||
<blockquote className="text-3xl font-medium text-white leading-relaxed">
|
||||
{quote}
|
||||
</blockquote>
|
||||
<figcaption className="text-white/60 text-sm">
|
||||
— {author}
|
||||
{site && <span className="ml-1 text-white/40">· {site}</span>}
|
||||
</figcaption>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OnboardingLeftPanel() {
|
||||
return (
|
||||
<div className="sticky top-0 h-screen overflow-hidden">
|
||||
{/* Carousel */}
|
||||
<div className="flex items-center justify-center h-full mt-24">
|
||||
<Carousel
|
||||
className="w-full h-full [&>div]:h-full [&>div]:min-h-full"
|
||||
opts={{
|
||||
loop: true,
|
||||
align: 'center',
|
||||
}}
|
||||
opts={{ loop: true, align: 'center' }}
|
||||
plugins={[Autoplay({ delay: 6000, stopOnInteraction: false })]}
|
||||
>
|
||||
<CarouselContent className="h-full">
|
||||
{onboardingSellingPoints.map((point, index) => (
|
||||
<CarouselItem
|
||||
key={`onboarding-point-${point.key}`}
|
||||
className="p-8 pb-32 pt-0"
|
||||
>
|
||||
{testimonials.map((t) => (
|
||||
<CarouselItem key={t.key} className="p-8 pb-32 pt-0">
|
||||
<div className="rounded-xl min-h-full h-full overflow-hidden bg-card border border-border shadow-lg">
|
||||
{point.render()}
|
||||
<TestimonialSlide
|
||||
bgImage={t.bgImage}
|
||||
quote={t.quote}
|
||||
author={t.author}
|
||||
site={t.site}
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious className="left-12 bottom-30 top-auto" />
|
||||
<CarouselNext className="right-12 bottom-30 top-auto" />
|
||||
</Carousel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useAppParams } from '@/hooks/use-app-params';
|
||||
import { useEventQueryFilters } from '@/hooks/use-event-query-filters';
|
||||
import { eventQueryFiltersParser } from '@/hooks/use-event-query-filters';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { Widget, WidgetBody } from '../widget';
|
||||
import { WidgetFooter, WidgetHeadSearchable } from './overview-widget';
|
||||
import {
|
||||
@@ -23,7 +26,9 @@ export default function OverviewTopEvents({
|
||||
shareId,
|
||||
}: OverviewTopEventsProps) {
|
||||
const { range, startDate, endDate } = useOverviewOptions();
|
||||
const [filters, setFilter] = useEventQueryFilters();
|
||||
const [filters] = useEventQueryFilters();
|
||||
const { organizationId } = useAppParams();
|
||||
const navigate = useNavigate();
|
||||
const trpc = useTRPC();
|
||||
const { data: conversions } = useQuery(
|
||||
trpc.overview.topConversions.queryOptions({ projectId, shareId }),
|
||||
@@ -162,11 +167,23 @@ export default function OverviewTopEvents({
|
||||
<OverviewWidgetTableEvents
|
||||
data={filteredData}
|
||||
onItemClick={(name) => {
|
||||
if (widget.meta?.type === 'linkOut') {
|
||||
setFilter('properties.href', name);
|
||||
} else {
|
||||
setFilter('name', name);
|
||||
}
|
||||
const filterName =
|
||||
widget.meta?.type === 'linkOut'
|
||||
? 'properties.href'
|
||||
: 'name';
|
||||
const f = eventQueryFiltersParser.serialize([
|
||||
{
|
||||
id: filterName,
|
||||
name: filterName,
|
||||
operator: 'is',
|
||||
value: [name],
|
||||
},
|
||||
]);
|
||||
navigate({
|
||||
to: '/$organizationId/$projectId/events/events',
|
||||
params: { organizationId, projectId },
|
||||
search: { f },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -18,19 +18,22 @@ import type { PaginationState, Table, Updater } from '@tanstack/react-table';
|
||||
import { getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import { memo } from 'react';
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
type Props = {
|
||||
query: UseQueryResult<RouterOutputs['profile']['list'], unknown>;
|
||||
type: 'profiles' | 'power-users';
|
||||
pageSize?: number;
|
||||
};
|
||||
|
||||
const LOADING_DATA = [{}, {}, {}, {}, {}, {}, {}, {}, {}] as IServiceProfile[];
|
||||
|
||||
export const ProfilesTable = memo(
|
||||
({ type, query }: Props) => {
|
||||
({ type, query, pageSize = PAGE_SIZE }: Props) => {
|
||||
const { data, isLoading } = query;
|
||||
const columns = useColumns(type);
|
||||
|
||||
const { setPage, state: pagination } = useDataTablePagination();
|
||||
const { setPage, state: pagination } = useDataTablePagination(pageSize);
|
||||
const {
|
||||
columnVisibility,
|
||||
setColumnVisibility,
|
||||
@@ -83,7 +86,7 @@ export const ProfilesTable = memo(
|
||||
</>
|
||||
);
|
||||
},
|
||||
arePropsEqual(['query.isLoading', 'query.data', 'type']),
|
||||
arePropsEqual(['query.isLoading', 'query.data', 'type', 'pageSize']),
|
||||
);
|
||||
|
||||
function ProfileTableToolbar({ table }: { table: Table<IServiceProfile> }) {
|
||||
|
||||
@@ -24,12 +24,12 @@ export const Route = createFileRoute(
|
||||
function Component() {
|
||||
const { projectId } = Route.useParams();
|
||||
const trpc = useTRPC();
|
||||
const { page } = useDataTablePagination();
|
||||
const { page } = useDataTablePagination(50);
|
||||
const { debouncedSearch } = useSearchQueryState();
|
||||
const query = useQuery(
|
||||
trpc.profile.list.queryOptions(
|
||||
{
|
||||
cursor: (page - 1) * 50,
|
||||
cursor: page - 1,
|
||||
projectId,
|
||||
take: 50,
|
||||
search: debouncedSearch,
|
||||
|
||||
@@ -31,7 +31,7 @@ function Component() {
|
||||
const query = useQuery(
|
||||
trpc.profile.list.queryOptions(
|
||||
{
|
||||
cursor: (page - 1) * 50,
|
||||
cursor: page - 1,
|
||||
projectId,
|
||||
take: 50,
|
||||
search: debouncedSearch,
|
||||
|
||||
@@ -23,11 +23,11 @@ export const Route = createFileRoute(
|
||||
function Component() {
|
||||
const { projectId } = Route.useParams();
|
||||
const trpc = useTRPC();
|
||||
const { page } = useDataTablePagination();
|
||||
const { page } = useDataTablePagination(50);
|
||||
const query = useQuery(
|
||||
trpc.profile.powerUsers.queryOptions(
|
||||
{
|
||||
cursor: (page - 1) * 50,
|
||||
cursor: page - 1,
|
||||
projectId,
|
||||
take: 50,
|
||||
},
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
import { IMPORT_PROVIDERS } from '@openpanel/importer/providers';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
Download,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
XCircleIcon,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import {
|
||||
IntegrationCard,
|
||||
@@ -19,21 +31,9 @@ import { Tooltiper } from '@/components/ui/tooltip';
|
||||
import { useAppParams } from '@/hooks/use-app-params';
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { pushModal } from '@/modals';
|
||||
import { IMPORT_PROVIDERS } from '@openpanel/importer/providers';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
Download,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
XCircleIcon,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const Route = createFileRoute(
|
||||
'/_app/$organizationId/$projectId/settings/_tabs/imports',
|
||||
'/_app/$organizationId/$projectId/settings/_tabs/imports'
|
||||
)({
|
||||
component: ImportsSettings,
|
||||
});
|
||||
@@ -48,8 +48,8 @@ function ImportsSettings() {
|
||||
{ projectId },
|
||||
{
|
||||
refetchInterval: 5000,
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
const imports = importsQuery.data ?? [];
|
||||
|
||||
@@ -61,7 +61,7 @@ function ImportsSettings() {
|
||||
});
|
||||
queryClient.invalidateQueries(trpc.import.list.pathFilter());
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const retryImport = useMutation(
|
||||
@@ -72,11 +72,11 @@ function ImportsSettings() {
|
||||
});
|
||||
queryClient.invalidateQueries(trpc.import.list.pathFilter());
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const handleProviderSelect = (
|
||||
provider: (typeof IMPORT_PROVIDERS)[number],
|
||||
provider: (typeof IMPORT_PROVIDERS)[number]
|
||||
) => {
|
||||
pushModal('AddImport', {
|
||||
provider: provider.id,
|
||||
@@ -93,10 +93,10 @@ function ImportsSettings() {
|
||||
failed: 'destructive',
|
||||
};
|
||||
const icons: Record<string, React.ReactNode> = {
|
||||
pending: <Loader2Icon className="w-4 h-4 animate-spin" />,
|
||||
processing: <Loader2Icon className="w-4 h-4 animate-spin" />,
|
||||
completed: <CheckCircleIcon className="w-4 h-4" />,
|
||||
failed: <XCircleIcon className="w-4 h-4" />,
|
||||
pending: <Loader2Icon className="h-4 w-4 animate-spin" />,
|
||||
processing: <Loader2Icon className="h-4 w-4 animate-spin" />,
|
||||
completed: <CheckCircleIcon className="h-4 w-4" />,
|
||||
failed: <XCircleIcon className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
if (status === 'failed') {
|
||||
@@ -105,7 +105,7 @@ function ImportsSettings() {
|
||||
content={errorMessage}
|
||||
tooltipClassName="max-w-xs break-words"
|
||||
>
|
||||
<Badge variant={variants[status] || 'default'} className="capitalize">
|
||||
<Badge className="capitalize" variant={variants[status] || 'default'}>
|
||||
{icons[status] || null}
|
||||
{status}
|
||||
</Badge>
|
||||
@@ -114,7 +114,7 @@ function ImportsSettings() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge variant={variants[status] || 'default'} className="capitalize">
|
||||
<Badge className="capitalize" variant={variants[status] || 'default'}>
|
||||
{icons[status] || null}
|
||||
{status}
|
||||
</Badge>
|
||||
@@ -124,26 +124,26 @@ function ImportsSettings() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{IMPORT_PROVIDERS.map((provider) => (
|
||||
<IntegrationCard
|
||||
key={provider.id}
|
||||
description={provider.description}
|
||||
icon={
|
||||
<IntegrationCardLogoImage
|
||||
src={provider.logo}
|
||||
backgroundColor={provider.backgroundColor}
|
||||
className="p-4"
|
||||
src={provider.logo}
|
||||
/>
|
||||
}
|
||||
key={provider.id}
|
||||
name={provider.name}
|
||||
description={provider.description}
|
||||
>
|
||||
<IntegrationCardFooter className="row justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => handleProviderSelect(provider)}
|
||||
variant="ghost"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Import Data
|
||||
</Button>
|
||||
</IntegrationCardFooter>
|
||||
@@ -153,9 +153,9 @@ function ImportsSettings() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Import History</h3>
|
||||
<h3 className="mb-4 font-medium text-lg">Import History</h3>
|
||||
|
||||
<div className="border rounded-lg">
|
||||
<div className="rounded-lg border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -172,8 +172,8 @@ function ImportsSettings() {
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>
|
||||
<FullPageEmptyState
|
||||
title="No imports yet"
|
||||
description="Your import history will appear here."
|
||||
title="No imports yet"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -196,7 +196,7 @@ function ImportsSettings() {
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
</TableCell>
|
||||
<TableCell className="text-right justify-end row">
|
||||
<TableCell className="row justify-end text-right">
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -204,9 +204,9 @@ function ImportsSettings() {
|
||||
{imports.map((imp) => (
|
||||
<TableRow key={imp.id}>
|
||||
<TableCell className="font-medium capitalize">
|
||||
<div className="row gap-2 items-center">
|
||||
<div className="row items-center gap-2">
|
||||
<div>{imp.config.provider}</div>
|
||||
<Badge variant="outline" className="uppercase">
|
||||
<Badge className="uppercase" variant="outline">
|
||||
{imp.config.type}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -220,7 +220,7 @@ function ImportsSettings() {
|
||||
<div className="space-y-1">
|
||||
{getStatusBadge(imp.status, imp.errorMessage)}
|
||||
{imp.statusMessage && (
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
<div className="truncate text-muted-foreground text-xs">
|
||||
{imp.statusMessage}
|
||||
</div>
|
||||
)}
|
||||
@@ -237,13 +237,13 @@ function ImportsSettings() {
|
||||
tooltipClassName="max-w-xs"
|
||||
>
|
||||
{imp.totalEvents.toLocaleString()}{' '}
|
||||
<InfoIcon className="w-4 h-4 inline-block relative -top-px" />
|
||||
<InfoIcon className="relative -top-px inline-block h-4 w-4" />
|
||||
</Tooltiper>
|
||||
</div>
|
||||
{imp.status === 'processing' && (
|
||||
<div className="w-full bg-secondary rounded-full h-1.5">
|
||||
<div className="h-1.5 w-full rounded-full bg-secondary">
|
||||
<div
|
||||
className="bg-primary h-1.5 rounded-full transition-all"
|
||||
className="h-1.5 rounded-full bg-primary transition-all"
|
||||
style={{
|
||||
width: `${Math.min(Math.round((imp.processedEvents / imp.totalEvents) * 100), 100)}%`,
|
||||
}}
|
||||
@@ -265,7 +265,7 @@ function ImportsSettings() {
|
||||
<TableCell>
|
||||
<Tooltiper
|
||||
content={
|
||||
<pre className="font-mono text-sm leading-normal whitespace-pre-wrap break-words">
|
||||
<pre className="whitespace-pre-wrap break-words font-mono text-sm leading-normal">
|
||||
{JSON.stringify(imp.config, null, 2)}
|
||||
</pre>
|
||||
}
|
||||
@@ -274,20 +274,20 @@ function ImportsSettings() {
|
||||
<Badge>Config</Badge>
|
||||
</Tooltiper>
|
||||
</TableCell>
|
||||
<TableCell className="text-right space-x-2">
|
||||
<TableCell className="space-x-2 text-right">
|
||||
{imp.status === 'failed' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => retryImport.mutate({ id: imp.id })}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => deleteImport.mutate({ id: imp.id })}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
|
||||
import { LoginLeftPanel } from '@/components/login-left-panel';
|
||||
import { LoginNavbar } from '@/components/login-navbar';
|
||||
import { OnboardingLeftPanel } from '@/components/onboarding-left-panel';
|
||||
|
||||
export const Route = createFileRoute('/_login')({
|
||||
beforeLoad: async ({ context }) => {
|
||||
@@ -16,7 +16,7 @@ function AuthLayout() {
|
||||
<div className="relative grid min-h-screen md:grid-cols-2">
|
||||
<LoginNavbar />
|
||||
<div className="hidden md:block">
|
||||
<LoginLeftPanel />
|
||||
<OnboardingLeftPanel />
|
||||
</div>
|
||||
<div className="center-center mx-auto w-full max-w-md px-4">
|
||||
<Outlet />
|
||||
|
||||
@@ -57,11 +57,11 @@ function Component() {
|
||||
<div className="col w-full gap-8 text-left">
|
||||
<div>
|
||||
<h1 className="mb-2 font-bold text-3xl text-foreground">
|
||||
Create an account
|
||||
Start tracking in minutes
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Let's start with creating your account. By creating an account you
|
||||
accept the{' '}
|
||||
Join 1,000+ projects already using OpenPanel. By creating an account
|
||||
you accept the{' '}
|
||||
<a
|
||||
className="underline transition-colors hover:text-foreground"
|
||||
href="https://openpanel.dev/terms"
|
||||
@@ -111,6 +111,9 @@ function Component() {
|
||||
<SignInGithub inviteId={inviteId} type="sign-up" />
|
||||
<SignInGoogle inviteId={inviteId} type="sign-up" />
|
||||
</div>
|
||||
<p className="text-center text-muted-foreground text-xs">
|
||||
No credit card required · Free 30-day trial · Cancel anytime
|
||||
</p>
|
||||
|
||||
<Or className="my-6" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user