mirror of
https://github.com/tcsenpai/pensieve.git
synced 2025-06-07 11:45:25 +00:00
feat(web): support time filter
This commit is contained in:
parent
d0cfd91f5a
commit
08c51a8259
@ -1,7 +1,7 @@
|
|||||||
<!-- Modal.svelte -->
|
<!-- Modal.svelte -->
|
||||||
<script>
|
<script>
|
||||||
import { ScrollArea } from "$lib/components/ui/scroll-area";
|
import { ScrollArea } from "$lib/components/ui/scroll-area";
|
||||||
import CopyToClipboard from "$lib/components/copy-to-clipboard.svelte"
|
import CopyToClipboard from "$lib/components/CopyToClipboard.svelte"
|
||||||
import OCRTable from './OCRTable.svelte';
|
import OCRTable from './OCRTable.svelte';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
114
web/src/lib/components/TimeFilter.svelte
Normal file
114
web/src/lib/components/TimeFilter.svelte
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import { RangeCalendar } from '$lib/components/ui/range-calendar/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CalendarDate,
|
||||||
|
DateFormatter,
|
||||||
|
type DateValue,
|
||||||
|
getLocalTimeZone
|
||||||
|
} from '@internationalized/date';
|
||||||
|
|
||||||
|
let now = +new Date();
|
||||||
|
|
||||||
|
const rangeMap = {
|
||||||
|
unlimited: {
|
||||||
|
label: '不限时间',
|
||||||
|
start: null,
|
||||||
|
end: null
|
||||||
|
},
|
||||||
|
today: {
|
||||||
|
label: '今天',
|
||||||
|
start: now - 24 * 60 * 60 * 1000,
|
||||||
|
end: now
|
||||||
|
},
|
||||||
|
week: {
|
||||||
|
label: '最近一周',
|
||||||
|
start: now - 7 * 24 * 60 * 60 * 1000,
|
||||||
|
end: now
|
||||||
|
},
|
||||||
|
month: {
|
||||||
|
label: '最近一个月',
|
||||||
|
start: now - 30 * 24 * 60 * 60 * 1000,
|
||||||
|
end: now
|
||||||
|
},
|
||||||
|
threeMonths: {
|
||||||
|
label: '最近三个月',
|
||||||
|
start: now - 90 * 24 * 60 * 60 * 1000,
|
||||||
|
end: now
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
label: '自定义',
|
||||||
|
start: null,
|
||||||
|
end: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let timeFilter = 'unlimited';
|
||||||
|
export let start: number;
|
||||||
|
export let end: number;
|
||||||
|
|
||||||
|
let customDateRange = {
|
||||||
|
start: null,
|
||||||
|
end: null
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateCustomDateRange(range: { start: DateValue | null; end: DateValue | null }) {
|
||||||
|
if (range.start && range.end) {
|
||||||
|
rangeMap.custom.start = range.start.toDate(getLocalTimeZone()).getTime();
|
||||||
|
rangeMap.custom.end = range.end.toDate(getLocalTimeZone()).getTime();
|
||||||
|
if (timeFilter !== 'custom') {
|
||||||
|
timeFilter = 'custom';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: updateCustomDateRange(customDateRange);
|
||||||
|
|
||||||
|
$: if (timeFilter !== 'custom') {
|
||||||
|
customDateRange = {
|
||||||
|
start: null,
|
||||||
|
end: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$: displayText =
|
||||||
|
(timeFilter === 'custom' && customDateRange.start && customDateRange.end)
|
||||||
|
? `${customDateRange.start?.toString()} - ${customDateRange.end?.toString()}`
|
||||||
|
: rangeMap[timeFilter].label;
|
||||||
|
$: start = rangeMap[timeFilter].start;
|
||||||
|
$: end = rangeMap[timeFilter].end;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild let:builder>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="border p-2 text-xs font-medium focus:outline-none"
|
||||||
|
builders={[builder]}
|
||||||
|
>
|
||||||
|
<span class="truncate">{displayText}</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content class="w-56">
|
||||||
|
<DropdownMenu.Label>时间筛选</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.RadioGroup bind:value={timeFilter}>
|
||||||
|
<DropdownMenu.RadioItem value="unlimited">时间不限</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem value="today">今天</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem value="week">最近一周</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem value="month">最近一个月</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.RadioItem value="threeMonths">最近三个月</DropdownMenu.RadioItem>
|
||||||
|
<DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.SubTrigger>自定义</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.SubContent>
|
||||||
|
<RangeCalendar bind:value={customDateRange} />
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
@ -11,15 +11,37 @@
|
|||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
import { RangeCalendar } from "$lib/components/ui/range-calendar/index.js";
|
import { RangeCalendar } from "$lib/components/ui/range-calendar/index.js";
|
||||||
import * as Popover from "$lib/components/ui/popover/index.js";
|
import * as Popover from "$lib/components/ui/popover/index.js";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
const df = new DateFormatter("en-US", {
|
const df = new DateFormatter("en-US", {
|
||||||
dateStyle: "medium"
|
dateStyle: "medium"
|
||||||
});
|
});
|
||||||
|
|
||||||
let value: DateRange | undefined = {
|
export let startTimestamp: number;
|
||||||
start: new CalendarDate(2022, 1, 20),
|
export let endTimestamp: number;
|
||||||
end: new CalendarDate(2022, 1, 20).add({ days: 20 })
|
|
||||||
};
|
let value: DateRange | undefined;
|
||||||
|
let initialized = false; // Flag to control reactive updates
|
||||||
|
let userSelected = false; // New flag to track user selection
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = now.getMonth() + 1; // getMonth() returns 0-11
|
||||||
|
const date = now.getDate();
|
||||||
|
value = {
|
||||||
|
start: new CalendarDate(year, month, date).subtract({ days: 30 }),
|
||||||
|
end: new CalendarDate(year, month, date)
|
||||||
|
};
|
||||||
|
console.log(initialized);
|
||||||
|
initialized = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if (initialized && value && value.start && value.end && !userSelected) {
|
||||||
|
userSelected = true; // Set flag when user selects a range
|
||||||
|
startTimestamp = value.start.toDate(getLocalTimeZone()).getTime() / 1000;
|
||||||
|
endTimestamp = value.end.toDate(getLocalTimeZone()).getTime() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
let startValue: DateValue | undefined = undefined;
|
let startValue: DateValue | undefined = undefined;
|
||||||
</script>
|
</script>
|
||||||
@ -30,13 +52,15 @@
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class={cn(
|
class={cn(
|
||||||
"w-[300px] justify-start text-left font-normal",
|
"w-[220px] justify-start text-center font-normal text-xs",
|
||||||
!value && "text-muted-foreground"
|
(startTimestamp === -1 && endTimestamp === -1) && "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
builders={[builder]}
|
builders={[builder]}
|
||||||
>
|
>
|
||||||
<Calendar class="mr-2 h-4 w-4" />
|
<Calendar class="mr-2 h-4 w-4" />
|
||||||
{#if value && value.start}
|
{#if startTimestamp === -1 && endTimestamp === -1}
|
||||||
|
不限时间
|
||||||
|
{:else if value && value.start}
|
||||||
{#if value.end}
|
{#if value.end}
|
||||||
{df.format(value.start.toDate(getLocalTimeZone()))} - {df.format(
|
{df.format(value.start.toDate(getLocalTimeZone()))} - {df.format(
|
||||||
value.end.toDate(getLocalTimeZone())
|
value.end.toDate(getLocalTimeZone())
|
@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import Check from "lucide-svelte/icons/check";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
|
||||||
|
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let checked: $$Props["checked"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
bind:checked
|
||||||
|
class={cn(
|
||||||
|
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerdown
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove
|
||||||
|
>
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.CheckboxIndicator>
|
||||||
|
<Check class="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.CheckboxIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn, flyAndScale } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.ContentProps;
|
||||||
|
type $$Events = DropdownMenuPrimitive.ContentEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let sideOffset: $$Props["sideOffset"] = 4;
|
||||||
|
export let transition: $$Props["transition"] = flyAndScale;
|
||||||
|
export let transitionConfig: $$Props["transitionConfig"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
{transition}
|
||||||
|
{transitionConfig}
|
||||||
|
{sideOffset}
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-md focus:outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:keydown
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.Content>
|
@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.ItemProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
};
|
||||||
|
type $$Events = DropdownMenuPrimitive.ItemEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let inset: $$Props["inset"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
class={cn(
|
||||||
|
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerdown
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.Item>
|
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.LabelProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let inset: $$Props["inset"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.Label>
|
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
|
||||||
|
|
||||||
|
export let value: $$Props["value"] = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.RadioGroup>
|
@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import Circle from "lucide-svelte/icons/circle";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.RadioItemProps;
|
||||||
|
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let value: $$Props["value"];
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
class={cn(
|
||||||
|
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{value}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerdown
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove
|
||||||
|
>
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.RadioIndicator>
|
||||||
|
<Circle class="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.RadioIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.SeparatorProps;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
class={cn("bg-muted -mx-1 my-1 h-px", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</span>
|
@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import { cn, flyAndScale } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.SubContentProps;
|
||||||
|
type $$Events = DropdownMenuPrimitive.SubContentEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let transition: $$Props["transition"] = flyAndScale;
|
||||||
|
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||||
|
x: -10,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
{transition}
|
||||||
|
{transitionConfig}
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:keydown
|
||||||
|
on:focusout
|
||||||
|
on:pointermove
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.SubContent>
|
@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
};
|
||||||
|
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let inset: $$Props["inset"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
class={cn(
|
||||||
|
"data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<ChevronRight class="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
48
web/src/lib/components/ui/dropdown-menu/index.ts
Normal file
48
web/src/lib/components/ui/dropdown-menu/index.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||||
|
import Item from "./dropdown-menu-item.svelte";
|
||||||
|
import Label from "./dropdown-menu-label.svelte";
|
||||||
|
import Content from "./dropdown-menu-content.svelte";
|
||||||
|
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||||
|
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||||
|
import Separator from "./dropdown-menu-separator.svelte";
|
||||||
|
import RadioGroup from "./dropdown-menu-radio-group.svelte";
|
||||||
|
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||||
|
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||||
|
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||||
|
|
||||||
|
const Sub = DropdownMenuPrimitive.Sub;
|
||||||
|
const Root = DropdownMenuPrimitive.Root;
|
||||||
|
const Trigger = DropdownMenuPrimitive.Trigger;
|
||||||
|
const Group = DropdownMenuPrimitive.Group;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sub,
|
||||||
|
Root,
|
||||||
|
Item,
|
||||||
|
Label,
|
||||||
|
Group,
|
||||||
|
Trigger,
|
||||||
|
Content,
|
||||||
|
Shortcut,
|
||||||
|
Separator,
|
||||||
|
RadioItem,
|
||||||
|
SubContent,
|
||||||
|
SubTrigger,
|
||||||
|
RadioGroup,
|
||||||
|
CheckboxItem,
|
||||||
|
//
|
||||||
|
Root as DropdownMenu,
|
||||||
|
Sub as DropdownMenuSub,
|
||||||
|
Item as DropdownMenuItem,
|
||||||
|
Label as DropdownMenuLabel,
|
||||||
|
Group as DropdownMenuGroup,
|
||||||
|
Content as DropdownMenuContent,
|
||||||
|
Trigger as DropdownMenuTrigger,
|
||||||
|
Shortcut as DropdownMenuShortcut,
|
||||||
|
RadioItem as DropdownMenuRadioItem,
|
||||||
|
Separator as DropdownMenuSeparator,
|
||||||
|
RadioGroup as DropdownMenuRadioGroup,
|
||||||
|
SubContent as DropdownMenuSubContent,
|
||||||
|
SubTrigger as DropdownMenuSubTrigger,
|
||||||
|
CheckboxItem as DropdownMenuCheckboxItem,
|
||||||
|
};
|
34
web/src/lib/components/ui/select/index.ts
Normal file
34
web/src/lib/components/ui/select/index.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
import Label from "./select-label.svelte";
|
||||||
|
import Item from "./select-item.svelte";
|
||||||
|
import Content from "./select-content.svelte";
|
||||||
|
import Trigger from "./select-trigger.svelte";
|
||||||
|
import Separator from "./select-separator.svelte";
|
||||||
|
|
||||||
|
const Root = SelectPrimitive.Root;
|
||||||
|
const Group = SelectPrimitive.Group;
|
||||||
|
const Input = SelectPrimitive.Input;
|
||||||
|
const Value = SelectPrimitive.Value;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Group,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Item,
|
||||||
|
Value,
|
||||||
|
Content,
|
||||||
|
Trigger,
|
||||||
|
Separator,
|
||||||
|
//
|
||||||
|
Root as Select,
|
||||||
|
Group as SelectGroup,
|
||||||
|
Input as SelectInput,
|
||||||
|
Label as SelectLabel,
|
||||||
|
Item as SelectItem,
|
||||||
|
Value as SelectValue,
|
||||||
|
Content as SelectContent,
|
||||||
|
Trigger as SelectTrigger,
|
||||||
|
Separator as SelectSeparator,
|
||||||
|
};
|
39
web/src/lib/components/ui/select/select-content.svelte
Normal file
39
web/src/lib/components/ui/select/select-content.svelte
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { scale } from "svelte/transition";
|
||||||
|
import { cn, flyAndScale } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = SelectPrimitive.ContentProps;
|
||||||
|
type $$Events = SelectPrimitive.ContentEvents;
|
||||||
|
|
||||||
|
export let sideOffset: $$Props["sideOffset"] = 4;
|
||||||
|
export let inTransition: $$Props["inTransition"] = flyAndScale;
|
||||||
|
export let inTransitionConfig: $$Props["inTransitionConfig"] = undefined;
|
||||||
|
export let outTransition: $$Props["outTransition"] = scale;
|
||||||
|
export let outTransitionConfig: $$Props["outTransitionConfig"] = {
|
||||||
|
start: 0.95,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
{inTransition}
|
||||||
|
{inTransitionConfig}
|
||||||
|
{outTransition}
|
||||||
|
{outTransitionConfig}
|
||||||
|
{sideOffset}
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground relative z-50 min-w-[8rem] overflow-hidden rounded-md border shadow-md outline-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:keydown
|
||||||
|
>
|
||||||
|
<div class="w-full p-1">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</SelectPrimitive.Content>
|
40
web/src/lib/components/ui/select/select-item.svelte
Normal file
40
web/src/lib/components/ui/select/select-item.svelte
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Check from "lucide-svelte/icons/check";
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = SelectPrimitive.ItemProps;
|
||||||
|
type $$Events = SelectPrimitive.ItemEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let value: $$Props["value"];
|
||||||
|
export let label: $$Props["label"] = undefined;
|
||||||
|
export let disabled: $$Props["disabled"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
{value}
|
||||||
|
{disabled}
|
||||||
|
{label}
|
||||||
|
class={cn(
|
||||||
|
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove
|
||||||
|
>
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check class="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<slot>
|
||||||
|
{label || value}
|
||||||
|
</slot>
|
||||||
|
</SelectPrimitive.Item>
|
16
web/src/lib/components/ui/select/select-label.svelte
Normal file
16
web/src/lib/components/ui/select/select-label.svelte
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = SelectPrimitive.LabelProps;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
class={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectPrimitive.Label>
|
11
web/src/lib/components/ui/select/select-separator.svelte
Normal file
11
web/src/lib/components/ui/select/select-separator.svelte
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = SelectPrimitive.SeparatorProps;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Separator class={cn("bg-muted -mx-1 my-1 h-px", className)} {...$$restProps} />
|
27
web/src/lib/components/ui/select/select-trigger.svelte
Normal file
27
web/src/lib/components/ui/select/select-trigger.svelte
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = SelectPrimitive.TriggerProps;
|
||||||
|
type $$Events = SelectPrimitive.TriggerEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
class={cn(
|
||||||
|
"border-input bg-background ring-offset-background focus-visible:ring-ring aria-[invalid]:border-destructive data-[placeholder]:[&>span]:text-muted-foreground flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
let:builder
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
>
|
||||||
|
<slot {builder} />
|
||||||
|
<div>
|
||||||
|
<ChevronDown class="h-4 w-4 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</SelectPrimitive.Trigger>
|
@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import Figure from '$lib/Figure.svelte';
|
import Figure from '$lib/Figure.svelte';
|
||||||
|
import TimeFilter from '$lib/components/TimeFilter.svelte';
|
||||||
import { Input } from "$lib/components/ui/input";
|
import { Input } from "$lib/components/ui/input";
|
||||||
import { PUBLIC_API_ENDPOINT } from '$env/static/public';
|
import { PUBLIC_API_ENDPOINT } from '$env/static/public';
|
||||||
|
|
||||||
@ -12,8 +13,9 @@
|
|||||||
let debounceTimer;
|
let debounceTimer;
|
||||||
let showModal = false;
|
let showModal = false;
|
||||||
let selectedImage = 0;
|
let selectedImage = 0;
|
||||||
let datetimeFilterStart = '';
|
|
||||||
let datetimeFilterEnd = '';
|
let startTimestamp = -1;
|
||||||
|
let endTimestamp = -1;
|
||||||
|
|
||||||
const debounceDelay = 300;
|
const debounceDelay = 300;
|
||||||
const apiEndpoint =
|
const apiEndpoint =
|
||||||
@ -22,11 +24,18 @@
|
|||||||
/**
|
/**
|
||||||
* @param {string} query
|
* @param {string} query
|
||||||
*/
|
*/
|
||||||
async function searchItems(query) {
|
async function searchItems(query, start, end) {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiEndpoint}/search?q=${encodeURIComponent(query)}`);
|
let url = `${apiEndpoint}/search?q=${encodeURIComponent(query)}`;
|
||||||
|
if (start > 0) {
|
||||||
|
url += `&start=${Math.floor(start / 1000)}`;
|
||||||
|
}
|
||||||
|
if (end > 0) {
|
||||||
|
url += `&end=${Math.floor(end / 1000)}`;
|
||||||
|
}
|
||||||
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Network response was not ok');
|
throw new Error('Network response was not ok');
|
||||||
}
|
}
|
||||||
@ -41,10 +50,10 @@
|
|||||||
/**
|
/**
|
||||||
* @param {string} query
|
* @param {string} query
|
||||||
*/
|
*/
|
||||||
function debounceSearch(query) {
|
function debounceSearch(query, start, end) {
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
searchItems(query);
|
searchItems(query, start, end);
|
||||||
}, debounceDelay);
|
}, debounceDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,10 +104,14 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
$: if (searchString.trim()) {
|
$: if (searchString.trim()) {
|
||||||
debounceSearch(searchString);
|
debounceSearch(searchString, startTimestamp, endTimestamp);
|
||||||
} else {
|
} else {
|
||||||
searchResults = [];
|
searchResults = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if ((startTimestamp !== -1 || endTimestamp !== -1) && searchString.trim()) {
|
||||||
|
debounceSearch(searchString, startTimestamp, endTimestamp);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeydown} />
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
@ -110,6 +123,7 @@
|
|||||||
bind:value={searchString}
|
bind:value={searchString}
|
||||||
placeholder="Type to search..."
|
placeholder="Type to search..."
|
||||||
/>
|
/>
|
||||||
|
<TimeFilter bind:start={startTimestamp} bind:end={endTimestamp} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user