fix(dashboard): pagination and login

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-03-01 13:33:55 +01:00
parent b801d6a8ef
commit 6251d143d1
11 changed files with 218 additions and 221 deletions

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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>

View File

@@ -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 },
});
}}
/>
)}

View File

@@ -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> }) {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
},

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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" />