fix: dashboard improvements and query speed improvements

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-01-09 14:42:11 +01:00
parent 4867260ece
commit cabfb1f3f0
49 changed files with 3398 additions and 950 deletions

View File

@@ -0,0 +1,82 @@
import { PromptCard } from '@/components/organization/prompt-card';
import { Button } from '@/components/ui/button';
import { useAppContext } from '@/hooks/use-app-context';
import { useCookieStore } from '@/hooks/use-cookie-store';
import { op } from '@/utils/op';
import { MessageSquareIcon } from 'lucide-react';
import { useEffect, useMemo } from 'react';
const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30;
export default function FeedbackPrompt() {
const { isSelfHosted } = useAppContext();
const [feedbackPromptSeen, setFeedbackPromptSeen] = useCookieStore(
'feedback-prompt-seen',
'',
{ maxAge: THIRTY_DAYS_IN_SECONDS },
);
const shouldShow = useMemo(() => {
if (isSelfHosted) {
return false;
}
if (!feedbackPromptSeen) {
return true;
}
try {
const lastSeenDate = new Date(feedbackPromptSeen);
const now = new Date();
const daysSinceLastSeen =
(now.getTime() - lastSeenDate.getTime()) / (1000 * 60 * 60 * 24);
return daysSinceLastSeen >= 30;
} catch {
// If date parsing fails, show the prompt
return true;
}
}, [isSelfHosted, feedbackPromptSeen]);
const handleGiveFeedback = () => {
// Open userjot widget
if (typeof window !== 'undefined' && 'uj' in window) {
(window.uj as any).showWidget();
}
// Set cookie with current timestamp
setFeedbackPromptSeen(new Date().toISOString());
op.track('feedback_prompt_button_clicked');
};
const handleClose = () => {
// Set cookie with current timestamp when closed
setFeedbackPromptSeen(new Date().toISOString());
};
useEffect(() => {
if (shouldShow) {
op.track('feedback_prompt_viewed');
}
}, [shouldShow]);
return (
<PromptCard
title="Share Your Feedback"
subtitle="Help us improve OpenPanel with your insights"
onClose={handleClose}
show={shouldShow}
gradientColor="rgb(59 130 246)"
>
<div className="px-6 col gap-4">
<p className="text-sm text-foreground leading-normal">
Your feedback helps us build features you actually need. Share your
thoughts, report bugs, or suggest improvements
</p>
<Button className="self-start" onClick={handleGiveFeedback}>
Give Feedback
</Button>
</div>
</PromptCard>
);
}

View File

@@ -0,0 +1,66 @@
import { Button } from '@/components/ui/button';
import { AnimatePresence, motion } from 'framer-motion';
import { XIcon } from 'lucide-react';
interface PromptCardProps {
title: string;
subtitle: string;
onClose: () => void;
children: React.ReactNode;
gradientColor?: string;
show: boolean;
}
export function PromptCard({
title,
subtitle,
onClose,
children,
gradientColor = 'rgb(16 185 129)',
show,
}: PromptCardProps) {
return (
<AnimatePresence>
{show && (
<motion.div
initial={{ opacity: 0, x: 100, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 100, scale: 0.95 }}
transition={{
type: 'spring',
stiffness: 300,
damping: 30,
}}
className="fixed bottom-0 right-0 z-50 p-4 max-w-sm"
>
<div className="bg-card border rounded-lg shadow-[0_0_100px_50px_rgba(20,20,20,1)] col gap-6 py-6 overflow-hidden">
<div className="relative px-6 col gap-1">
<div
className="absolute -bottom-10 -right-10 h-64 w-64 rounded-full opacity-30 blur-3xl pointer-events-none"
style={{
background: `radial-gradient(circle, ${gradientColor} 0%, transparent 70%)`,
}}
/>
<div className="row items-center justify-between">
<h2 className="text-xl font-semibold max-w-[200px] leading-snug">
{title}
</h2>
<Button
variant="ghost"
size="icon"
className="rounded-full"
onClick={onClose}
>
<XIcon className="size-4" />
</Button>
</div>
<p className="text-sm text-muted-foreground">{subtitle}</p>
</div>
{children}
</div>
</motion.div>
)}
</AnimatePresence>
);
}

View File

@@ -1,7 +1,7 @@
import { Button, LinkButton } from '@/components/ui/button';
import { PromptCard } from '@/components/organization/prompt-card';
import { LinkButton } from '@/components/ui/button';
import { useAppContext } from '@/hooks/use-app-context';
import { useCookieStore } from '@/hooks/use-cookie-store';
import { AnimatePresence, motion } from 'framer-motion';
import {
AwardIcon,
HeartIcon,
@@ -9,7 +9,6 @@ import {
MessageCircleIcon,
RocketIcon,
SparklesIcon,
XIcon,
ZapIcon,
} from 'lucide-react';
@@ -78,70 +77,43 @@ export default function SupporterPrompt() {
}
return (
<AnimatePresence>
{!supporterPromptClosed && (
<motion.div
initial={{ opacity: 0, x: 100, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 100, scale: 0.95 }}
transition={{
type: 'spring',
stiffness: 300,
damping: 30,
}}
className="fixed bottom-0 right-0 z-50 p-4 max-w-md"
<PromptCard
title="Support OpenPanel"
subtitle="Help us build the future of open analytics"
onClose={() => setSupporterPromptClosed(true)}
show={!supporterPromptClosed}
gradientColor="rgb(16 185 129)"
>
<div className="col gap-3 px-6">
{PERKS.map((perk) => (
<PerkPoint
key={perk.text}
icon={perk.icon}
text={perk.text}
description={perk.description}
/>
))}
</div>
<div className="px-6">
<LinkButton
className="w-full"
href="https://buy.polar.sh/polar_cl_Az1CruNFzQB2bYdMOZmGHqTevW317knWqV44W1FqZmV"
>
<div className="bg-card border p-6 rounded-lg shadow-lg col gap-4">
<div>
<div className="row items-center justify-between">
<h2 className="text-xl font-semibold">Support OpenPanel</h2>
<Button
variant="ghost"
size="icon"
className="rounded-full"
onClick={() => setSupporterPromptClosed(true)}
>
<XIcon className="size-4" />
</Button>
</div>
<p className="text-sm text-muted-foreground">
Help us build the future of open analytics
</p>
</div>
<div className="col gap-3">
{PERKS.map((perk) => (
<PerkPoint
key={perk.text}
icon={perk.icon}
text={perk.text}
description={perk.description}
/>
))}
</div>
<div className="pt-2">
<LinkButton
className="w-full"
href="https://buy.polar.sh/polar_cl_Az1CruNFzQB2bYdMOZmGHqTevW317knWqV44W1FqZmV"
>
Become a Supporter
</LinkButton>
<p className="text-xs text-muted-foreground text-center mt-4">
Starting at $20/month Cancel anytime {' '}
<a
href="https://openpanel.dev/supporter"
target="_blank"
rel="noreferrer"
className="text-primary underline-offset-4 hover:underline"
>
Learn more
</a>
</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
Become a Supporter
</LinkButton>
<p className="text-xs text-muted-foreground text-center mt-4">
Starting at $20/month Cancel anytime {' '}
<a
href="https://openpanel.dev/supporter"
target="_blank"
rel="noreferrer"
className="text-primary underline-offset-4 hover:underline"
>
Learn more
</a>
</p>
</div>
</PromptCard>
);
}