modifications in ui for audio recording display and some backend fixes

This commit is contained in:
tdv
2025-09-03 11:55:34 +03:00
parent 35c5a4dc46
commit 247a2ed6b2
36 changed files with 944 additions and 64 deletions

View File

@@ -1,27 +1,29 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Primitive, type PrimitiveProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { type ButtonVariants, buttonVariants } from '.'
import type { PrimitiveProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import type { ButtonVariants } from "."
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from "."
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
variant?: ButtonVariants["variant"]
size?: ButtonVariants["size"]
class?: HTMLAttributes["class"]
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
as: "button",
})
</script>
<template>
<Primitive
data-slot="button"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>
</script>
<template>
<Primitive
data-slot="button"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -1,34 +1,35 @@
import { cva, type VariantProps } from 'class-variance-authority'
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Button } from './Button.vue'
export { default as Button } from "./Button.vue"
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: 'default',
size: 'default',
variant: "default",
size: "default",
},
},
)

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { NumberFieldRootEmits, NumberFieldRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { NumberFieldRoot, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<NumberFieldRootProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<NumberFieldRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<NumberFieldRoot v-bind="forwarded" :class="cn('grid gap-1.5', props.class)">
<slot />
</NumberFieldRoot>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn('relative [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5 [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { NumberFieldDecrementProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Minus } from "lucide-vue-next"
import { NumberFieldDecrement, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<NumberFieldDecrementProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<NumberFieldDecrement data-slot="decrement" v-bind="forwarded" :class="cn('absolute top-1/2 -translate-y-1/2 left-0 p-3 disabled:cursor-not-allowed disabled:opacity-20', props.class)">
<slot>
<Minus class="h-4 w-4" />
</slot>
</NumberFieldDecrement>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { NumberFieldIncrementProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Plus } from "lucide-vue-next"
import { NumberFieldIncrement, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<NumberFieldIncrementProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<NumberFieldIncrement data-slot="increment" v-bind="forwarded" :class="cn('absolute top-1/2 -translate-y-1/2 right-0 disabled:cursor-not-allowed disabled:opacity-20 p-3', props.class)">
<slot>
<Plus class="h-4 w-4" />
</slot>
</NumberFieldIncrement>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { NumberFieldInput } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<NumberFieldInput
data-slot="input"
:class="cn('flex h-9 w-full rounded-md border border-input bg-transparent py-1 text-sm text-center shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)"
/>
</template>

View File

@@ -0,0 +1,5 @@
export { default as NumberField } from "./NumberField.vue"
export { default as NumberFieldContent } from "./NumberFieldContent.vue"
export { default as NumberFieldDecrement } from "./NumberFieldDecrement.vue"
export { default as NumberFieldIncrement } from "./NumberFieldIncrement.vue"
export { default as NumberFieldInput } from "./NumberFieldInput.vue"

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { PopoverRootEmits, PopoverRootProps } from "reka-ui"
import { PopoverRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<PopoverRootProps>()
const emits = defineEmits<PopoverRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<PopoverRoot
data-slot="popover"
v-bind="forwarded"
>
<slot />
</PopoverRoot>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { PopoverAnchorProps } from "reka-ui"
import { PopoverAnchor } from "reka-ui"
const props = defineProps<PopoverAnchorProps>()
</script>
<template>
<PopoverAnchor
data-slot="popover-anchor"
v-bind="props"
>
<slot />
</PopoverAnchor>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import type { PopoverContentEmits, PopoverContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
PopoverContent,
PopoverPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<PopoverContentProps & { class?: HTMLAttributes["class"] }>(),
{
align: "center",
sideOffset: 4,
},
)
const emits = defineEmits<PopoverContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<PopoverPortal>
<PopoverContent
data-slot="popover-content"
v-bind="{ ...forwarded, ...$attrs }"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
props.class,
)
"
>
<slot />
</PopoverContent>
</PopoverPortal>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { PopoverTriggerProps } from "reka-ui"
import { PopoverTrigger } from "reka-ui"
const props = defineProps<PopoverTriggerProps>()
</script>
<template>
<PopoverTrigger
data-slot="popover-trigger"
v-bind="props"
>
<slot />
</PopoverTrigger>
</template>

View File

@@ -0,0 +1,4 @@
export { default as Popover } from "./Popover.vue"
export { default as PopoverAnchor } from "./PopoverAnchor.vue"
export { default as PopoverContent } from "./PopoverContent.vue"
export { default as PopoverTrigger } from "./PopoverTrigger.vue"

View File

@@ -0,0 +1,62 @@
<script lang="ts" setup>
import type { RangeCalendarRootEmits, RangeCalendarRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { RangeCalendarRoot, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"
import { RangeCalendarCell, RangeCalendarCellTrigger, RangeCalendarGrid, RangeCalendarGridBody, RangeCalendarGridHead, RangeCalendarGridRow, RangeCalendarHeadCell, RangeCalendarHeader, RangeCalendarHeading, RangeCalendarNextButton, RangeCalendarPrevButton } from "."
const props = defineProps<RangeCalendarRootProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<RangeCalendarRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<RangeCalendarRoot
v-slot="{ grid, weekDays }"
data-slot="range-calendar"
:class="cn('p-3', props.class)"
v-bind="forwarded"
>
<RangeCalendarHeader>
<RangeCalendarHeading />
<div class="flex items-center gap-1">
<RangeCalendarPrevButton />
<RangeCalendarNextButton />
</div>
</RangeCalendarHeader>
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
<RangeCalendarGrid v-for="month in grid" :key="month.value.toString()">
<RangeCalendarGridHead>
<RangeCalendarGridRow>
<RangeCalendarHeadCell
v-for="day in weekDays" :key="day"
>
{{ day }}
</RangeCalendarHeadCell>
</RangeCalendarGridRow>
</RangeCalendarGridHead>
<RangeCalendarGridBody>
<RangeCalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="mt-2 w-full">
<RangeCalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
>
<RangeCalendarCellTrigger
:day="weekDate"
:month="month.value"
/>
</RangeCalendarCell>
</RangeCalendarGridRow>
</RangeCalendarGridBody>
</RangeCalendarGrid>
</div>
</RangeCalendarRoot>
</template>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import type { RangeCalendarCellProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { RangeCalendarCell, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<RangeCalendarCellProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<RangeCalendarCell
data-slot="range-calendar-cell"
:class="cn('relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:bg-accent first:[&:has([data-selected])]:rounded-l-md last:[&:has([data-selected])]:rounded-r-md [&:has([data-selected][data-selection-end])]:rounded-r-md [&:has([data-selected][data-selection-start])]:rounded-l-md', props.class)"
v-bind="forwardedProps"
>
<slot />
</RangeCalendarCell>
</template>

View File

@@ -0,0 +1,41 @@
<script lang="ts" setup>
import type { RangeCalendarCellTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { RangeCalendarCellTrigger, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from '@/components/ui/button'
const props = withDefaults(defineProps<RangeCalendarCellTriggerProps & { class?: HTMLAttributes["class"] }>(), {
as: "button",
})
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<RangeCalendarCellTrigger
data-slot="range-calendar-trigger"
:class="cn(
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal data-[selected]:opacity-100',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
// Selection Start
'data-[selection-start]:bg-primary data-[selection-start]:text-primary-foreground data-[selection-start]:hover:bg-primary data-[selection-start]:hover:text-primary-foreground data-[selection-start]:focus:bg-primary data-[selection-start]:focus:text-primary-foreground',
// Selection End
'data-[selection-end]:bg-primary data-[selection-end]:text-primary-foreground data-[selection-end]:hover:bg-primary data-[selection-end]:hover:text-primary-foreground data-[selection-end]:focus:bg-primary data-[selection-end]:focus:text-primary-foreground',
// Outside months
'data-[outside-view]:text-muted-foreground',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
props.class,
)"
v-bind="forwardedProps"
>
<slot />
</RangeCalendarCellTrigger>
</template>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import type { RangeCalendarGridProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { RangeCalendarGrid, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<RangeCalendarGridProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<RangeCalendarGrid
data-slot="range-calendar-grid"
:class="cn('w-full border-collapse space-x-1', props.class)"
v-bind="forwardedProps"
>
<slot />
</RangeCalendarGrid>
</template>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import type { RangeCalendarGridBodyProps } from "reka-ui"
import { RangeCalendarGridBody } from "reka-ui"
const props = defineProps<RangeCalendarGridBodyProps>()
</script>
<template>
<RangeCalendarGridBody
data-slot="range-calendar-grid-body"
v-bind="props"
>
<slot />
</RangeCalendarGridBody>
</template>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import type { RangeCalendarGridHeadProps } from "reka-ui"
import { RangeCalendarGridHead } from "reka-ui"
const props = defineProps<RangeCalendarGridHeadProps>()
</script>
<template>
<RangeCalendarGridHead
data-slot="range-calendar-grid-head"
v-bind="props"
>
<slot />
</RangeCalendarGridHead>
</template>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import type { RangeCalendarGridRowProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { RangeCalendarGridRow, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<RangeCalendarGridRowProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<RangeCalendarGridRow
data-slot="range-calendar-grid-row"
:class="cn('flex', props.class)" v-bind="forwardedProps"
>
<slot />
</RangeCalendarGridRow>
</template>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import type { RangeCalendarHeadCellProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { RangeCalendarHeadCell, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<RangeCalendarHeadCellProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<RangeCalendarHeadCell
data-slot="range-calendar-head-cell"
:class="cn('w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)"
v-bind="forwardedProps"
>
<slot />
</RangeCalendarHeadCell>
</template>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import type { RangeCalendarHeaderProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { RangeCalendarHeader, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<RangeCalendarHeaderProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<RangeCalendarHeader
data-slot="range-calendar-header"
:class="cn('flex justify-center pt-1 relative items-center w-full', props.class)"
v-bind="forwardedProps"
>
<slot />
</RangeCalendarHeader>
</template>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import type { RangeCalendarHeadingProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { RangeCalendarHeading, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<RangeCalendarHeadingProps & { class?: HTMLAttributes["class"] }>()
defineSlots<{
default: (props: { headingValue: string }) => any
}>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<RangeCalendarHeading
v-slot="{ headingValue }"
data-slot="range-calendar-heading"
:class="cn('text-sm font-medium', props.class)"
v-bind="forwardedProps"
>
<slot :heading-value>
{{ headingValue }}
</slot>
</RangeCalendarHeading>
</template>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import type { RangeCalendarNextProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronRight } from "lucide-vue-next"
import { RangeCalendarNext, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<RangeCalendarNextProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<RangeCalendarNext
data-slot="range-calendar-next-button"
:class="cn(
buttonVariants({ variant: 'outline' }),
'absolute right-1',
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)"
v-bind="forwardedProps"
>
<slot>
<ChevronRight class="size-4" />
</slot>
</RangeCalendarNext>
</template>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import type { RangeCalendarPrevProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronLeft } from "lucide-vue-next"
import { RangeCalendarPrev, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<RangeCalendarPrevProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<RangeCalendarPrev
data-slot="range-calendar-prev-button"
:class="cn(
buttonVariants({ variant: 'outline' }),
'absolute left-1',
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)"
v-bind="forwardedProps"
>
<slot>
<ChevronLeft class="size-4" />
</slot>
</RangeCalendarPrev>
</template>

View File

@@ -0,0 +1,12 @@
export { default as RangeCalendar } from "./RangeCalendar.vue"
export { default as RangeCalendarCell } from "./RangeCalendarCell.vue"
export { default as RangeCalendarCellTrigger } from "./RangeCalendarCellTrigger.vue"
export { default as RangeCalendarGrid } from "./RangeCalendarGrid.vue"
export { default as RangeCalendarGridBody } from "./RangeCalendarGridBody.vue"
export { default as RangeCalendarGridHead } from "./RangeCalendarGridHead.vue"
export { default as RangeCalendarGridRow } from "./RangeCalendarGridRow.vue"
export { default as RangeCalendarHeadCell } from "./RangeCalendarHeadCell.vue"
export { default as RangeCalendarHeader } from "./RangeCalendarHeader.vue"
export { default as RangeCalendarHeading } from "./RangeCalendarHeading.vue"
export { default as RangeCalendarNextButton } from "./RangeCalendarNextButton.vue"
export { default as RangeCalendarPrevButton } from "./RangeCalendarPrevButton.vue"

View File

@@ -0,0 +1,54 @@
<script setup lang="ts">
import type { DateRange } from 'reka-ui'
import {
DateFormatter,
getLocalTimeZone,
} from '@internationalized/date'
import { CalendarIcon } from 'lucide-vue-next'
import { type Ref, ref } from 'vue'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { RangeCalendar } from '@/components/ui/range-calendar'
const df = new DateFormatter('en-US', {
dateStyle: 'medium',
})
const value = ref({
start: undefined,
end: undefined,
}) as Ref<DateRange>
</script>
<template>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-[280px] justify-start text-left font-normal',
!value.start && 'text-muted-foreground',
)"
>
<CalendarIcon class="mr-2 h-4 w-4" />
<template v-if="value.start">
<template v-if="value.end">
{{ df.format(value.start.toDate(getLocalTimeZone())) }} - {{ df.format(value.end.toDate(getLocalTimeZone())) }}
</template>
<template v-else>
{{ df.format(value.start.toDate(getLocalTimeZone())) }}
</template>
</template>
<template v-else>
Pick a date
</template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<RangeCalendar v-model="value" initial-focus :number-of-months="2" @update:start-value="(startDate) => value.start = startDate" />
</PopoverContent>
</Popover>
</template>

View File

@@ -1,10 +1,21 @@
<script setup lang="ts">
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import DeviceDashboard from './DeviceDashboard.vue';
import { Button } from '@/components/ui/button';
import DeviceRecordings from './DeviceRecordings.vue';
import { type PropType } from 'vue';
const props = defineProps({
guid: { type: String as PropType<string>, required: true },
})
</script>
<template>
<Tabs default-value="records">
<TabsList class="space-x-8">
<TabsTrigger value="dashboard">
Dashboard
</TabsTrigger>
<TabsTrigger value="records">
Records
</TabsTrigger>
@@ -12,11 +23,19 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
Livestream
</TabsTrigger>
</TabsList>
<TabsContent value="dashboard">
<DeviceDashboard/>
</TabsContent>
<TabsContent value="records">
<DeviceRecordings :guid="guid"/>
</TabsContent>
<TabsContent value="livestream">
<Button>
Start streaming
</Button>
<Button>
Stop streaming
</Button>
</TabsContent>
</Tabs>
</template>

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import {
NumberField,
NumberFieldContent,
NumberFieldDecrement,
NumberFieldIncrement,
NumberFieldInput,
} from '@/components/ui/number-field'
import AssignDevice from "./AssignDevice.vue";
import { ref } from "vue";
import Separator from "@/components/ui/separator/Separator.vue";
import DataRangePicker from "./DataRangePicker.vue";
const selectedUserIds = ref<string[]>([])
const usrIDs = selectedUserIds.value
</script>
<template>
<Card>
<CardHeader>
<CardTitle>
Dashboard
</CardTitle>
</CardHeader>
<CardContent>
<Label for="users">Allowed users</Label>
<AssignDevice v-model="usrIDs"></AssignDevice>
<Button>Start recording</Button>
<Button>Stop recording</Button>
<NumberField id="duration" :default-value="120" :min="30">
<Label for="duration"> Record duration in seconds</Label>
<NumberFieldContent>
<NumberFieldDecrement></NumberFieldDecrement>
<NumberFieldInput></NumberFieldInput>
<NumberFieldIncrement></NumberFieldIncrement>
</NumberFieldContent>
</NumberField>
<Separator>Communication settings</Separator>
<NumberField id="polling" :default-value="60" :min="30">
<Label for="polling"> Timeout interwal in seconds </Label>
<NumberFieldContent>
<NumberFieldDecrement></NumberFieldDecrement>
<NumberFieldInput></NumberFieldInput>
<NumberFieldIncrement></NumberFieldIncrement>
</NumberFieldContent>
</NumberField>
<NumberField id="jitter" :default-value="10" :min="5">
<Label for="jitter"> Jitter in seconds </Label>
<NumberFieldContent>
<NumberFieldDecrement></NumberFieldDecrement>
<NumberFieldInput></NumberFieldInput>
<NumberFieldIncrement></NumberFieldIncrement>
</NumberFieldContent>
</NumberField>
<Separator></Separator>
<Label for="sleepdate">Date/time to sleep</Label>
<DataRangePicker id="sleepdate"></DataRangePicker>
</CardContent>
</Card>
</template>

View File

@@ -0,0 +1,158 @@
<script setup lang="ts">
import { ScrollArea } from '@/components/ui/scroll-area';
import { api } from '@/lib/api';
import { ref, computed, onMounted, watch } from 'vue';
import type { PropType } from 'vue';
type DeviceRecord = {
id: number
startedAt: number // unix seconds
stoppedAt: number // unix seconds
}
type ListResp = {
records: DeviceRecord[]
offset: number
limit: number
total: number
}
const props = defineProps({
guid: { type: String as PropType<string>, required: true },
pageSize: { type: Number as PropType<number>, default: 50 },
// If your API base is set via Vite (e.g. "/api"), it will be used automatically.
apiBase: {
type: String as PropType<string>,
default: (import.meta as any)?.env?.VITE_API_URL || ''
},
// Provide a custom builder if your record stream URL differs.
recordSrcBuilder: {
type: Function as PropType<(id: number) => string>,
default: undefined
}
})
const loading = ref(false)
const error = ref<string | null>(null)
const records = ref<DeviceRecord[]>([])
const offset = ref(0)
const total = ref(0)
const baseUrl = computed(() => (props.apiBase || '').replace(/\/+$/, ''))
const sortedRecords = computed(() =>
[...records.value].sort((a, b) => b.startedAt - a.startedAt)
)
const dtf = new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short'
})
function formatDate(unix: number) {
return dtf.format(new Date(unix * 1000))
}
function durationMinutes(rec: DeviceRecord) {
const seconds = Math.max(0, (rec.stoppedAt ?? 0) - (rec.startedAt ?? 0))
return (seconds / 60).toFixed(2) // minutes with 2 decimals
}
function getRecordSrc(id: number) {
if (props.recordSrcBuilder) return props.recordSrcBuilder(id)
// Build from Axios baseURL (works for '/api' or 'https://host/api')
const base = (api.defaults.baseURL as string | undefined)?.replace(/\/+$/, '') ?? ''
return `${base}/records/${id}/file`
}
async function fetchRecords(reset = true) {
if (!props.guid) return
loading.value = true
error.value = null
try {
if (reset) {
offset.value = 0
records.value = []
}
const { data } = await api.get<ListResp>('/records', {
params: {
guid: props.guid,
offset: offset.value,
limit: props.pageSize,
},
})
records.value = reset ? data.records : [...records.value, ...data.records]
total.value = data.total ?? records.value.length
offset.value = (data.offset ?? offset.value) + (data.limit ?? props.pageSize)
} catch (err: any) {
// Axios-style error extraction
error.value =
(err?.response?.data?.error as string) ??
(err?.message as string) ??
'Failed to load recordings'
} finally {
loading.value = false
}
}
const hasMore = computed(() => offset.value < total.value)
function loadMore() {
if (!loading.value && hasMore.value) fetchRecords(false)
}
onMounted(() => {
fetchRecords(true)
})
watch(
() => props.guid,
() => fetchRecords(true),
{ immediate: false }
)
</script>
<template>
<ScrollArea class="w-full h-full">
<div class="p-3 space-y-3">
<div v-if="error" class="text-sm text-red-500 border border-red-200 rounded-md p-2">
{{ error }}
</div>
<div v-if="!loading && records.length === 0 && !error" class="text-sm text-muted-foreground">
No recordings found.
</div>
<ul v-else class="divide-y divide-border rounded-lg border">
<li v-for="rec in sortedRecords" :key="rec.id"
class="p-3 flex flex-col md:flex-row md:items-center md:justify-between gap-3">
<div class="space-y-1">
<div class="font-medium">Record #{{ rec.id }}</div>
<div class="text-sm text-muted-foreground">
{{ formatDate(rec.startedAt) }} {{ formatDate(rec.stoppedAt) }}
</div>
<div class="text-xs">
Duration: <span class="font-semibold">{{ durationMinutes(rec) }}</span> min
</div>
</div>
<!-- Optional playback; update getRecordSrc via prop if your URL differs -->
<audio class="w-full md:w-auto" :src="getRecordSrc(rec.id)" controls preload="none">
Your browser does not support the audio element.
</audio>
</li>
</ul>
<div class="flex items-center gap-3">
<button v-if="hasMore" class="px-3 py-2 rounded-md border text-sm hover:bg-accent" @click="loadMore"
:disabled="loading">
{{ loading ? 'Loading…' : 'Load more' }}
</button>
<span v-else class="text-xs text-muted-foreground">All loaded</span>
</div>
</div>
</ScrollArea>
</template>

View File

@@ -2,12 +2,15 @@
import { useColorMode } from '@vueuse/core'
import Navbar from '@/customcompometns/Navbar.vue';
import DeviceComponent from '@/customcompometns/DeviceComponent.vue';
import { useRoute } from 'vue-router';
import { computed } from 'vue';
const mode = useColorMode()
const route = useRoute()
const guid = computed(() => String(route.params.guid ?? route.query.guid ?? ''))
</script>
<template>
<Navbar>
<DeviceComponent></DeviceComponent>
<DeviceComponent :guid="guid"></DeviceComponent>
</Navbar>
</template>