Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 46 additions & 5 deletions bun.lock

Large diffs are not rendered by default.

162 changes: 162 additions & 0 deletions src/ui/app/components/alert/alert-hover-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { StatusBadge } from "@/components/alert/status-badge";
import { TriggerActionIndicator } from "@/components/alert/trigger-action";
import AppIcon from "@/components/app/app-icon";
import TagIcon from "@/components/tag/tag-icon";
import { DurationText } from "@/components/time/duration-text";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator";
import { Text } from "@/components/ui/text";
import { useApp, useTag } from "@/hooks/use-refresh";
import type { Alert } from "@/lib/entities";
import { timeFrameToLabel } from "@/lib/entities";
import { ArrowRightIcon, ClockAlertIcon } from "lucide-react";
import { useMemo, type ReactNode } from "react";
import { NavLink } from "react-router";

export function AlertHoverCard({
alert,
children,
}: {
alert: Alert;
children: ReactNode;
}) {
return (
<HoverCard>
<HoverCardTrigger asChild>{children}</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" className="w-80">
<AlertHoverCardContent alert={alert} />
</HoverCardContent>
</HoverCard>
);
}

function AlertHoverCardContent({ alert }: { alert: Alert }) {
const app = useApp(alert.target.tag === "app" ? alert.target.id : null);
const tag = useTag(alert.target.tag === "tag" ? alert.target.id : null);
const targetEntity = app ?? tag;

const currentUsage = useMemo(() => {
const usages = alert.target.tag === "app" ? app?.usages : tag?.usages;
if (!usages) return 0;
switch (alert.timeFrame) {
case "daily":
return usages.today;
case "weekly":
return usages.week;
case "monthly":
return usages.month;
}
}, [alert, app, tag]);

const progress = Math.min(
(currentUsage / (alert.usageLimit || 1)) * 100,
100,
);

const targetLink = app ? `/apps/${app.id}` : tag ? `/tags/${tag.id}` : "#";

return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3">
{app ? (
<AppIcon app={app} className="w-10 h-10 shrink-0" />
) : tag ? (
<TagIcon tag={tag} className="w-10 h-10 shrink-0" />
) : null}
<div className="flex flex-col min-w-0 gap-1">
<NavLink
to={targetLink}
className="hover:underline"
onClick={(e) => e.stopPropagation()}
>
<Text className="font-semibold">
{targetEntity?.name ?? "Unknown"}
</Text>
</NavLink>
Comment thread
Enigmatrix marked this conversation as resolved.
<div className="flex items-center gap-2">
<StatusBadge status={alert.status} className="text-xs h-5" />
<TriggerActionIndicator
action={alert.triggerAction}
className="text-xs"
/>
</div>
</div>
</div>

<div className="space-y-1.5">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1.5">
<DurationText ticks={currentUsage} className="font-medium" />
<span className="text-muted-foreground">
({Math.round(progress)}%)
</span>
</div>
<div className="flex items-center gap-1 text-muted-foreground text-xs">
<DurationText ticks={alert.usageLimit} />
<span>/</span>
<span>{timeFrameToLabel(alert.timeFrame)}</span>
</div>
</div>
<Progress value={progress} className="h-1.5 rounded-sm" />
</div>

{alert.reminders.length > 0 && (
<>
<Separator />
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<ClockAlertIcon className="size-3.5" />
<span>
{alert.reminders.length} Reminder
{alert.reminders.length !== 1 ? "s" : ""}
</span>
</div>
</>
)}

{targetEntity && (
<>
<Separator />
<div className="grid grid-cols-3 gap-2 text-center">
<div>
<div className="text-xs text-muted-foreground">Today</div>
<DurationText
ticks={targetEntity.usages.today}
className="text-sm font-medium"
/>
</div>
<div>
<div className="text-xs text-muted-foreground">Week</div>
<DurationText
ticks={targetEntity.usages.week}
className="text-sm font-medium"
/>
</div>
<div>
<div className="text-xs text-muted-foreground">Month</div>
<DurationText
ticks={targetEntity.usages.month}
className="text-sm font-medium"
/>
</div>
</div>
</>
)}

<Separator />

<NavLink
to={`/alerts/${alert.id}`}
className="text-sm text-primary hover:underline inline-flex items-center gap-1 self-start"
onClick={(e) => e.stopPropagation()}
>
View Details
<ArrowRightIcon className="size-3" />
</NavLink>
</div>
);
}
48 changes: 30 additions & 18 deletions src/ui/app/components/alert/choose-target.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AppHoverCard } from "@/components/app/app-hover-card";
import AppIcon from "@/components/app/app-icon";
import { MiniTagItem } from "@/components/app/choose-multi-apps";
import {
Expand All @@ -9,6 +10,8 @@ import {
import { SearchBar } from "@/components/search-bar";
import { CreateTagDialog } from "@/components/tag/create-tag-dialog";
import { ScoreCircle } from "@/components/tag/score";
import { TagHoverCard } from "@/components/tag/tag-hover-card";
import TagIcon from "@/components/tag/tag-icon";
import { Button } from "@/components/ui/button";
import {
Popover,
Expand All @@ -23,7 +26,7 @@ import type { tagSchema } from "@/lib/schema";
import { useAppState } from "@/lib/state";
import { cn } from "@/lib/utils";
import { useConcatVirtualItems, useVirtualSection } from "@/lib/virtualization";
import { ChevronDown, ChevronRight, Plus, TagIcon } from "lucide-react";
import { ChevronDown, ChevronRight, Plus } from "lucide-react";
import {
useCallback,
useEffect,
Expand Down Expand Up @@ -107,12 +110,13 @@ export function ChooseTarget({
)}
onClick={() => onValueChanged({ tag: "tag", id: tag.id })}
>
<TagIcon
className="w-4 h-4 shrink-0"
style={{ color: tag.color }}
/>
<Text>{tag.name}</Text>
<ScoreCircle score={tag.score} />
<TagHoverCard tag={tag}>
<div className="inline-flex items-center gap-2 min-w-0">
<TagIcon tag={tag} className="w-4 h-4 shrink-0" />
<Text>{tag.name}</Text>
<ScoreCircle score={tag.score} />
</div>
</TagHoverCard>
</button>
</div>
),
Expand Down Expand Up @@ -196,8 +200,12 @@ export function ChooseTarget({
)}
onClick={() => onValueChanged({ tag: "app", id: app.id })}
>
<AppIcon app={app} className="w-4 h-4 shrink-0" />
<Text>{app.name}</Text>
<AppHoverCard app={app}>
<div className="inline-flex items-center gap-2 min-w-0">
<AppIcon app={app} className="w-4 h-4 shrink-0" />
<Text>{app.name}</Text>
</div>
</AppHoverCard>
Comment thread
Enigmatrix marked this conversation as resolved.
<MiniTagItem tagId={app.tagId} />
</button>
</div>
Expand Down Expand Up @@ -278,16 +286,20 @@ function ChooseTargetTrigger({
className={cn("flex gap-2 items-center", className)}
>
{value?.tag === "app" && app ? (
<>
<AppIcon app={app} className="w-5 h-5 shrink-0" />
<Text>{app.name}</Text>
</>
<AppHoverCard app={app}>
<span className="inline-flex items-center gap-2">
<AppIcon app={app} className="w-5 h-5 shrink-0" />
<Text>{app.name}</Text>
</span>
</AppHoverCard>
) : value?.tag === "tag" && tag ? (
<>
<TagIcon className="w-5 h-5 shrink-0" style={{ color: tag.color }} />
<Text>{tag.name}</Text>
<ScoreCircle score={tag.score} />
</>
<TagHoverCard tag={tag}>
<span className="inline-flex items-center gap-2">
<TagIcon tag={tag} className="w-5 h-5 shrink-0" />
<Text>{tag.name}</Text>
<ScoreCircle score={tag.score} />
</span>
</TagHoverCard>
) : (
(placeholder ?? (
<Text className="text-muted-foreground">Choose Target</Text>
Expand Down
138 changes: 138 additions & 0 deletions src/ui/app/components/app/app-hover-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import AppIcon from "@/components/app/app-icon";
import { ScoreCircle } from "@/components/tag/score";
import TagIcon from "@/components/tag/tag-icon";
import { DurationText } from "@/components/time/duration-text";
import { Badge } from "@/components/ui/badge";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Separator } from "@/components/ui/separator";
import { Text } from "@/components/ui/text";
import { useTag } from "@/hooks/use-refresh";
import type { App } from "@/lib/entities";
import { ArrowRightIcon } from "lucide-react";
import type { ReactNode } from "react";
import { NavLink } from "react-router";

export function AppHoverCard({
app,
children,
}: {
app: App;
children: ReactNode;
}) {
return (
<HoverCard>
<HoverCardTrigger asChild>{children}</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" className="w-80">
<AppHoverCardContent app={app} />
</HoverCardContent>
</HoverCard>
);
}

function AppHoverCardContent({ app }: { app: App }) {
const tag = useTag(app.tagId);

return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3">
<AppIcon app={app} className="w-10 h-10 shrink-0" />
<div className="flex flex-col min-w-0 gap-1">
<Text className="font-semibold">{app.name}</Text>
{app.identity.tag !== "website" && app.company && (
<Text className="text-sm text-muted-foreground">{app.company}</Text>
)}
</div>
</div>

{tag && (
<NavLink
to={`/tags/${tag.id}`}
className="inline-flex self-start"
onClick={(e) => e.stopPropagation()}
>
<Badge
variant="outline"
style={{
borderColor: tag.color,
color: tag.color,
backgroundColor: "rgba(255, 255, 255, 0.2)",
}}
className="whitespace-nowrap hover:opacity-80 transition-opacity"
>
<TagIcon tag={tag} className="size-3 mr-1" />
<Text className="max-w-32">{tag.name}</Text>
<ScoreCircle score={tag.score} className="ml-1.5" />
</Badge>
</NavLink>
)}

{app.description && (
<Text className="text-sm text-muted-foreground line-clamp-2">
{app.description}
</Text>
)}

<div className="text-xs inline-flex items-center border border-border rounded-md overflow-hidden bg-muted/30 self-start max-w-full">
<div className="bg-muted px-2 py-1 border-r border-border font-medium shrink-0">
{app.identity.tag === "uwp"
? "UWP"
: app.identity.tag === "win32"
? "Win32"
: app.identity.tag === "website"
? "Web"
: "Squirrel"}
</div>
<Text className="font-mono px-2 py-1 text-muted-foreground">
{app.identity.tag === "uwp"
? app.identity.aumid
: app.identity.tag === "win32"
? app.identity.path
: app.identity.tag === "website"
? app.identity.baseUrl
: app.identity.identifier}
</Text>
</div>

<Separator />

<div className="grid grid-cols-3 gap-2 text-center">
<div>
<div className="text-xs text-muted-foreground">Today</div>
<DurationText
ticks={app.usages.today}
className="text-sm font-medium"
/>
</div>
<div>
<div className="text-xs text-muted-foreground">Week</div>
<DurationText
ticks={app.usages.week}
className="text-sm font-medium"
/>
</div>
<div>
<div className="text-xs text-muted-foreground">Month</div>
<DurationText
ticks={app.usages.month}
className="text-sm font-medium"
/>
</div>
</div>

<Separator />

<NavLink
to={`/apps/${app.id}`}
className="text-sm text-primary hover:underline inline-flex items-center gap-1 self-start"
onClick={(e) => e.stopPropagation()}
>
View Details
<ArrowRightIcon className="size-3" />
</NavLink>
</div>
);
}
Loading
Loading