update
This commit is contained in:
@@ -1,54 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Moon, Sun, SunMoon } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@repo/ui/components/ui/dropdown-menu'
|
||||
import dark from './logo-dark.svg'
|
||||
import light from './logo-light.svg'
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
size="icon"
|
||||
className="cursor-pointer text-muted-foreground"
|
||||
>
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||
<span className="sr-only">Alternar tema</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="*:cursor-pointer">
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||
<Sun /> Claro
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||
<Moon /> Escuro
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||
<SunMoon /> Sistema
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export function ThemedImage() {
|
||||
return (
|
||||
<>
|
||||
<img src={light} className="h-8" data-hide-on-theme="dark" />
|
||||
<img src={dark} className="h-8" data-hide-on-theme="light" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import { CheckIcon } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Badge } from '@repo/ui/components/ui/badge'
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator
|
||||
} from '@repo/ui/components/ui/command'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from '@repo/ui/components/ui/popover'
|
||||
import { Separator } from '@repo/ui/components/ui/separator'
|
||||
import { cn } from '@repo/ui/lib/utils'
|
||||
|
||||
interface FacetedFilterProps<TData, TValue> {
|
||||
value?: string[]
|
||||
title?: string
|
||||
icon: React.ComponentType
|
||||
className?: string
|
||||
options: {
|
||||
label: string
|
||||
value: string
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
}[]
|
||||
onChange?: (values: string[]) => void
|
||||
}
|
||||
|
||||
export function FacetedFilter<TData, TValue>({
|
||||
value = [],
|
||||
icon: Icon,
|
||||
title,
|
||||
options,
|
||||
onChange,
|
||||
className
|
||||
}: FacetedFilterProps<TData, TValue>) {
|
||||
const [selectedValues, setSelectedValues] = useState(new Set(value))
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn('h-9 border-dashed cursor-pointer', className)}
|
||||
>
|
||||
<Icon />
|
||||
{title}
|
||||
{selectedValues?.size > 0 && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||
<div className="gap-1 flex">
|
||||
{selectedValues.size > 2 ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{selectedValues.size} selecionados
|
||||
</Badge>
|
||||
) : (
|
||||
options
|
||||
.filter((option) => selectedValues.has(option.value))
|
||||
.map((option) => (
|
||||
<Badge
|
||||
variant="outline"
|
||||
key={option.value}
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{option.label}
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder={title} />
|
||||
<CommandList>
|
||||
<CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedValues.has(option.value)
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
className="cursor-pointer"
|
||||
key={option.value}
|
||||
onSelect={() => {
|
||||
if (isSelected) {
|
||||
selectedValues.delete(option.value)
|
||||
} else {
|
||||
selectedValues.add(option.value)
|
||||
}
|
||||
|
||||
setSelectedValues(selectedValues)
|
||||
onChange?.(Array.from(selectedValues))
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-4 items-center justify-center rounded-[4px] border',
|
||||
isSelected
|
||||
? 'bg-primary border-primary text-primary-foreground'
|
||||
: 'border-input [&_svg]:invisible'
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="text-primary-foreground size-3.5" />
|
||||
</div>
|
||||
{option.icon && (
|
||||
<option.icon className="text-muted-foreground size-4" />
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandGroup>
|
||||
{selectedValues.size > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
setSelectedValues(new Set())
|
||||
onChange?.([])
|
||||
}}
|
||||
className="justify-center text-center cursor-pointer"
|
||||
>
|
||||
Limpar
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1072.73 329.6" width="111" height="36" title="EDUSEG">
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
fill="#8cd366"
|
||||
d="M152.18,217.62l-68.61-30.91c-1.88-1.2-4.28-1.2-6.16,0l-68.61,30.91c-3.81,2.43-8.8-.3-8.8-4.82V8.98C0,5.82,2.56,3.26,5.72,3.26h149.54c3.16,0,5.72,2.56,5.72,5.72v203.81c0,4.52-5,7.26-8.8,4.82Z"
|
||||
></path>
|
||||
<path fill="#2e3524" d="M93.97,74.01H26.61v20.16h67.36v-20.16Z"></path>
|
||||
<path fill="#2e3524" d="M107.44,111.16H26.61v26.8h80.83v-26.8Z"></path>
|
||||
<path fill="#2e3524" d="M107.44,30.27H26.61v26.8h80.83v-26.8Z"></path>
|
||||
<path
|
||||
fill="#2e3524"
|
||||
d="M134.38,131.23c0-3.72-3.02-6.73-6.73-6.73s-6.73,3.02-6.73,6.73,3.02,6.73,6.73,6.73,6.73-3.02,6.73-6.73Z"
|
||||
></path>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#f9f7e8" d="M244.7,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M362.72,3.24h57.79c10.71,0,20.47,2.35,29.29,7.06,8.83,4.7,15.79,11.18,20.87,19.39,5.08,8.21,7.63,17.45,7.63,27.67v214.88c0,10.22-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.83,4.7-18.73,7.08-29.7,7.08h-57.79V3.24ZM427.55,283.88c1.74-1.87,2.6-4.18,2.6-6.86V52.56c-.26-2.69-1.34-4.97-3.22-6.86-1.88-1.87-4.15-2.83-6.82-2.83h-14v243.85h14.41c2.93,0,5.27-.94,7.01-2.83h.02Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M531.5,322.49c-8.71-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67V3.24h48.15v279.41c0,2.69.93,4.99,2.82,6.86,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.86V3.24h48.16v272.21c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.7-18.73,7.08-29.7,7.08s-20.8-2.35-29.5-7.08l.05-.02Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M672.79,322.49c-8.7-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67v-78.75h48.16v85.95c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-77.88c0-5.66-2.22-10.3-6.63-13.94-4.41-3.62-11.57-8.02-21.47-13.13-8.3-4.3-15.05-8.14-20.27-11.52-5.22-3.36-9.71-7.87-13.45-13.54-3.75-5.66-5.63-12.24-5.63-19.8V54.12c0-10.22,2.53-19.44,7.63-27.67,5.08-8.21,11.97-14.66,20.68-19.39,8.68-4.7,18.53-7.06,29.5-7.06s20.87,2.35,29.69,7.06c8.83,4.7,15.72,11.18,20.68,19.39,4.96,8.21,7.42,17.45,7.42,27.67v71.09h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v69.79c0,6.19,2.34,11.26,7.04,15.14,4.67,3.91,12.24,8.83,22.68,14.74,8.04,4.32,14.57,8.09,19.68,11.3,5.08,3.24,9.37,7.46,12.83,12.72,3.48,5.26,5.22,11.26,5.22,17.98v86.83c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.71-18.72,7.08-29.7,7.08s-20.8-2.35-29.5-7.08Z"
|
||||
></path>
|
||||
<path fill="#f9f7e8" d="M784.56,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M920.63,322.49c-5.63-4.18-10.11-10.1-13.45-17.76-3.34-7.68-5.01-16.49-5.01-26.45V53.71c0-9.96,2.53-19.06,7.63-27.26,5.08-8.21,12.05-14.66,20.87-19.39,8.83-4.7,18.6-7.06,29.32-7.06s20.54,2.35,29.5,7.06c8.97,4.7,15.91,11.18,20.87,19.39,4.96,8.21,7.42,17.3,7.42,27.26v94.51h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v231.36c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-46.03h-11.64v-51.29h59.8v145.4h-48.16v-14.14c-2.96,5.4-6.82,9.48-11.64,12.31-4.82,2.83-10.83,4.25-18.06,4.25s-13.64-2.09-19.27-6.26l-.02-.02Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f9f7e8"
|
||||
d="M1053.27,25.05h-6.13l-.06-3.69h5.48c.83-.02,1.61-.15,2.33-.4.72-.27,1.3-.64,1.73-1.14.44-.51.65-1.14.65-1.87,0-.93-.16-1.67-.48-2.22-.3-.55-.83-.94-1.59-1.16-.74-.25-1.74-.37-3.01-.37h-3.78v20.42h-4.12V10.54h7.9c1.87,0,3.49.27,4.86.82,1.38.53,2.44,1.34,3.18,2.44.76,1.08,1.14,2.43,1.14,4.06,0,1.02-.24,1.93-.71,2.73-.47.8-1.17,1.49-2.1,2.07-.91.57-2.03,1.03-3.35,1.39-.06,0-.12.07-.2.2-.06.13-.11.2-.17.2-.32.19-.53.33-.63.43-.08.08-.16.12-.25.14-.08.02-.31.03-.68.03ZM1052.99,25.05l.6-2.81c2.95,0,4.97.64,6.05,1.93,1.08,1.27,1.62,2.89,1.62,4.86v1.53c0,.7.03,1.37.08,2.02.08.62.21,1.15.4,1.59v.45h-4.23c-.19-.49-.3-1.19-.34-2.1-.02-.91-.03-1.57-.03-1.99v-1.48c0-1.38-.31-2.39-.94-3.04s-1.69-.97-3.21-.97ZM1035.75,22.92c0,2.52.43,4.87,1.28,7.04.87,2.16,2.08,4.05,3.64,5.68,1.55,1.61,3.34,2.87,5.37,3.78,2.05.89,4.22,1.33,6.53,1.33s4.51-.44,6.53-1.33c2.03-.91,3.8-2.17,5.34-3.78,1.53-1.63,2.74-3.52,3.61-5.68.87-2.18,1.31-4.52,1.31-7.04s-.44-4.86-1.31-7.01c-.87-2.16-2.07-4.04-3.61-5.65-1.53-1.61-3.31-2.86-5.34-3.75-2.03-.91-4.2-1.36-6.53-1.36s-4.49.45-6.53,1.36c-2.03.89-3.81,2.14-5.37,3.75-1.55,1.61-2.77,3.49-3.64,5.65-.85,2.16-1.28,4.5-1.28,7.01ZM1032.4,22.92c0-3.01.52-5.8,1.56-8.38,1.04-2.57,2.49-4.82,4.34-6.73,1.86-1.93,4-3.43,6.42-4.49,2.44-1.08,5.06-1.62,7.84-1.62s5.39.54,7.81,1.62c2.44,1.06,4.58,2.56,6.42,4.49,1.86,1.91,3.31,4.16,4.35,6.73,1.06,2.57,1.59,5.37,1.59,8.38s-.53,5.8-1.59,8.38c-1.04,2.57-2.49,4.84-4.35,6.79-1.83,1.93-3.97,3.44-6.42,4.52-2.42,1.08-5.03,1.62-7.81,1.62s-5.39-.54-7.84-1.62c-2.42-1.08-4.56-2.58-6.42-4.52-1.85-1.95-3.3-4.21-4.34-6.79-1.04-2.57-1.56-5.37-1.56-8.38Z"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,43 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1072.73 329.6" width="111" height="36" title="EDUSEG">
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
fill="#8cd366"
|
||||
d="M152.18,217.62l-68.61-30.91c-1.88-1.2-4.28-1.2-6.16,0l-68.61,30.91c-3.81,2.43-8.8-.3-8.8-4.82V8.98C0,5.82,2.56,3.26,5.72,3.26h149.54c3.16,0,5.72,2.56,5.72,5.72v203.81c0,4.52-5,7.26-8.8,4.82Z"
|
||||
></path>
|
||||
<path fill="#2e3524" d="M93.97,74.01H26.61v20.16h67.36v-20.16Z"></path>
|
||||
<path fill="#2e3524" d="M107.44,111.16H26.61v26.8h80.83v-26.8Z"></path>
|
||||
<path fill="#2e3524" d="M107.44,30.27H26.61v26.8h80.83v-26.8Z"></path>
|
||||
<path
|
||||
fill="#2e3524"
|
||||
d="M134.38,131.23c0-3.72-3.02-6.73-6.73-6.73s-6.73,3.02-6.73,6.73,3.02,6.73,6.73,6.73,6.73-3.02,6.73-6.73Z"
|
||||
></path>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#2e3524" d="M244.7,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#2e3524"
|
||||
d="M362.72,3.24h57.79c10.71,0,20.47,2.35,29.29,7.06,8.83,4.7,15.79,11.18,20.87,19.39,5.08,8.21,7.63,17.45,7.63,27.67v214.88c0,10.22-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.83,4.7-18.73,7.08-29.7,7.08h-57.79V3.24ZM427.55,283.88c1.74-1.87,2.6-4.18,2.6-6.86V52.56c-.26-2.69-1.34-4.97-3.22-6.86-1.88-1.87-4.15-2.83-6.82-2.83h-14v243.85h14.41c2.93,0,5.27-.94,7.01-2.83h.02Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#2e3524"
|
||||
d="M531.5,322.49c-8.71-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67V3.24h48.15v279.41c0,2.69.93,4.99,2.82,6.86,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.86V3.24h48.16v272.21c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.7-18.73,7.08-29.7,7.08s-20.8-2.35-29.5-7.08l.05-.02Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#2e3524"
|
||||
d="M672.79,322.49c-8.7-4.7-15.6-11.16-20.68-19.39-5.08-8.21-7.63-17.42-7.63-27.67v-78.75h48.16v85.95c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-77.88c0-5.66-2.22-10.3-6.63-13.94-4.41-3.62-11.57-8.02-21.47-13.13-8.3-4.3-15.05-8.14-20.27-11.52-5.22-3.36-9.71-7.87-13.45-13.54-3.75-5.66-5.63-12.24-5.63-19.8V54.12c0-10.22,2.53-19.44,7.63-27.67,5.08-8.21,11.97-14.66,20.68-19.39,8.68-4.7,18.53-7.06,29.5-7.06s20.87,2.35,29.69,7.06c8.83,4.7,15.72,11.18,20.68,19.39,4.96,8.21,7.42,17.45,7.42,27.67v71.09h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v69.79c0,6.19,2.34,11.26,7.04,15.14,4.67,3.91,12.24,8.83,22.68,14.74,8.04,4.32,14.57,8.09,19.68,11.3,5.08,3.24,9.37,7.46,12.83,12.72,3.48,5.26,5.22,11.26,5.22,17.98v86.83c0,10.25-2.48,19.46-7.42,27.67-4.96,8.21-11.83,14.69-20.68,19.39-8.85,4.71-18.72,7.08-29.7,7.08s-20.8-2.35-29.5-7.08Z"
|
||||
></path>
|
||||
<path fill="#2e3524" d="M784.56,3.24h92.33v44.43h-44.15v88.85h39.38v39.62h-39.38v105.77h44.15v44.42h-92.33V3.24Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#2e3524"
|
||||
d="M920.63,322.49c-5.63-4.18-10.11-10.1-13.45-17.76-3.34-7.68-5.01-16.49-5.01-26.45V53.71c0-9.96,2.53-19.06,7.63-27.26,5.08-8.21,12.05-14.66,20.87-19.39,8.83-4.7,18.6-7.06,29.32-7.06s20.54,2.35,29.5,7.06c8.97,4.7,15.91,11.18,20.87,19.39,4.96,8.21,7.42,17.3,7.42,27.26v94.51h-48.16V46.92c0-2.69-.88-4.97-2.6-6.86-1.74-1.87-4.08-2.83-7.01-2.83-2.67,0-4.96.94-6.82,2.83-1.89,1.9-2.82,4.18-2.82,6.86v231.36c0,2.69.93,4.99,2.82,6.87,1.86,1.9,4.15,2.83,6.82,2.83,2.93,0,5.27-.94,7.01-2.83,1.74-1.87,2.6-4.18,2.6-6.87v-46.03h-11.64v-51.29h59.8v145.4h-48.16v-14.14c-2.96,5.4-6.82,9.48-11.64,12.31-4.82,2.83-10.83,4.25-18.06,4.25s-13.64-2.09-19.27-6.26l-.02-.02Z"
|
||||
></path>
|
||||
<path
|
||||
fill="#2e3524"
|
||||
d="M1053.27,25.05h-6.13l-.06-3.69h5.48c.83-.02,1.61-.15,2.33-.4.72-.27,1.3-.64,1.73-1.14.44-.51.65-1.14.65-1.87,0-.93-.16-1.67-.48-2.22-.3-.55-.83-.94-1.59-1.16-.74-.25-1.74-.37-3.01-.37h-3.78v20.42h-4.12V10.54h7.9c1.87,0,3.49.27,4.86.82,1.38.53,2.44,1.34,3.18,2.44.76,1.08,1.14,2.43,1.14,4.06,0,1.02-.24,1.93-.71,2.73-.47.8-1.17,1.49-2.1,2.07-.91.57-2.03,1.03-3.35,1.39-.06,0-.12.07-.2.2-.06.13-.11.2-.17.2-.32.19-.53.33-.63.43-.08.08-.16.12-.25.14-.08.02-.31.03-.68.03ZM1052.99,25.05l.6-2.81c2.95,0,4.97.64,6.05,1.93,1.08,1.27,1.62,2.89,1.62,4.86v1.53c0,.7.03,1.37.08,2.02.08.62.21,1.15.4,1.59v.45h-4.23c-.19-.49-.3-1.19-.34-2.1-.02-.91-.03-1.57-.03-1.99v-1.48c0-1.38-.31-2.39-.94-3.04s-1.69-.97-3.21-.97ZM1035.75,22.92c0,2.52.43,4.87,1.28,7.04.87,2.16,2.08,4.05,3.64,5.68,1.55,1.61,3.34,2.87,5.37,3.78,2.05.89,4.22,1.33,6.53,1.33s4.51-.44,6.53-1.33c2.03-.91,3.8-2.17,5.34-3.78,1.53-1.63,2.74-3.52,3.61-5.68.87-2.18,1.31-4.52,1.31-7.04s-.44-4.86-1.31-7.01c-.87-2.16-2.07-4.04-3.61-5.65-1.53-1.61-3.31-2.86-5.34-3.75-2.03-.91-4.2-1.36-6.53-1.36s-4.49.45-6.53,1.36c-2.03.89-3.81,2.14-5.37,3.75-1.55,1.61-2.77,3.49-3.64,5.65-.85,2.16-1.28,4.5-1.28,7.01ZM1032.4,22.92c0-3.01.52-5.8,1.56-8.38,1.04-2.57,2.49-4.82,4.34-6.73,1.86-1.93,4-3.43,6.42-4.49,2.44-1.08,5.06-1.62,7.84-1.62s5.39.54,7.81,1.62c2.44,1.06,4.58,2.56,6.42,4.49,1.86,1.91,3.31,4.16,4.35,6.73,1.06,2.57,1.59,5.37,1.59,8.38s-.53,5.8-1.59,8.38c-1.04,2.57-2.49,4.84-4.35,6.79-1.83,1.93-3.97,3.44-6.42,4.52-2.42,1.08-5.03,1.62-7.81,1.62s-5.39-.54-7.84-1.62c-2.42-1.08-4.56-2.58-6.42-4.52-1.85-1.95-3.3-4.21-4.34-6.79-1.04-2.57-1.56-5.37-1.56-8.38Z"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,140 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
CirclePlayIcon,
|
||||
DollarSignIcon,
|
||||
GraduationCapIcon,
|
||||
LogOutIcon,
|
||||
UserIcon
|
||||
} from 'lucide-react'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@repo/ui/components/ui/dropdown-menu'
|
||||
import { initials } from '@repo/ui/lib/utils'
|
||||
|
||||
export function NavUser({
|
||||
user
|
||||
}: {
|
||||
user: {
|
||||
name: string
|
||||
email: string
|
||||
scope: string
|
||||
}
|
||||
}) {
|
||||
const scopes = user.scope.split(' ')
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="cursor-pointer" asChild>
|
||||
<Avatar className="size-10">
|
||||
<AvatarFallback>{initials(user.name)}</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="size-10">
|
||||
<AvatarFallback>{initials(user.name)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
to="//scorm.eduseg.workers.dev/settings"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<UserIcon />
|
||||
Minha conta
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
to="//scorm.eduseg.workers.dev/payments"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<DollarSignIcon />
|
||||
Histórico de compras
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuGroup>
|
||||
{grantIfHas(['apps:admin', 'apps:studio'], scopes, 'any') && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-muted-foreground text-sm">
|
||||
Aplicações
|
||||
</DropdownMenuLabel>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="//scorm.eduseg.workers.dev" className="cursor-pointer">
|
||||
<GraduationCapIcon />
|
||||
Sala de aula
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{grantIfHas(['apps:studio'], scopes) && (
|
||||
<>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
to="//studio.saladeaula.digital"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<CirclePlayIcon />
|
||||
EDUSEG® Estúdio
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/logout" className="cursor-pointer" reloadDocument>
|
||||
<LogOutIcon />
|
||||
Sair
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function grantIfHas(
|
||||
required: string[],
|
||||
granted: string[],
|
||||
mode: 'all' | 'any' = 'all'
|
||||
): boolean {
|
||||
const grantedSet: Set<string> = new Set(granted)
|
||||
|
||||
if (mode === 'all') {
|
||||
return required.every((scope) => grantedSet.has(scope))
|
||||
}
|
||||
|
||||
return required.some((scope) => grantedSet.has(scope))
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { debounce } from 'lodash'
|
||||
import { SearchIcon, XIcon } from 'lucide-react'
|
||||
import { useRef } from 'react'
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput
|
||||
} from '@repo/ui/components/ui/input-group'
|
||||
|
||||
import { useKeyPress } from '@/hooks/use-keypress'
|
||||
import { cn } from '@repo/ui/lib/utils'
|
||||
|
||||
export function SearchForm({
|
||||
placeholder,
|
||||
className,
|
||||
onChange,
|
||||
defaultValue = '',
|
||||
...props
|
||||
}: {
|
||||
placeholder?: React.ReactNode
|
||||
className?: string
|
||||
onChange?: (value: string) => void
|
||||
defaultValue?: string
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useKeyPress('/', () => {
|
||||
inputRef.current?.focus()
|
||||
})
|
||||
|
||||
const debouncedOnChange = debounce((value: string) => {
|
||||
onChange?.(value)
|
||||
}, 200)
|
||||
|
||||
return (
|
||||
<InputGroup className="group">
|
||||
<InputGroupInput
|
||||
className={cn('peer', className)}
|
||||
placeholder=" "
|
||||
ref={inputRef}
|
||||
defaultValue={defaultValue}
|
||||
onChange={(e) => debouncedOnChange(e.target.value)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<InputGroupAddon>
|
||||
<SearchIcon />
|
||||
</InputGroupAddon>
|
||||
|
||||
{placeholder && (
|
||||
<InputGroupAddon className="font-normal hidden peer-focus-within:hidden peer-placeholder-shown:block">
|
||||
{placeholder}
|
||||
</InputGroupAddon>
|
||||
)}
|
||||
|
||||
{defaultValue && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = ''
|
||||
}
|
||||
onChange?.('')
|
||||
inputRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
<XIcon />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
)}
|
||||
</InputGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Skeleton as XSkeleton } from '@repo/ui/components/ui/skeleton'
|
||||
|
||||
export function Skeleton() {
|
||||
return (
|
||||
<div className="lg:max-w-2xl mx-auto flex flex-col space-y-3">
|
||||
<div className="space-y-2">
|
||||
<XSkeleton className="h-4 w-2/6" />
|
||||
<XSkeleton className="h-4 w-4/6" />
|
||||
<XSkeleton className="h-4 w-5/6" />
|
||||
<XSkeleton className="h-4 w-6/6" />
|
||||
<XSkeleton className="h-4 w-6/6" />
|
||||
<XSkeleton className="h-4 w-3/6" />
|
||||
<XSkeleton className="h-4 w-5/6" />
|
||||
<XSkeleton className="h-4 w-4/6" />
|
||||
<XSkeleton className="h-4 w-4/6" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { throttle } from 'lodash'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export function useKeyPress(targetKey: string, callback: CallableFunction) {
|
||||
useEffect(() => {
|
||||
const onKeyDown = throttle((event) => {
|
||||
if (event.key === targetKey) {
|
||||
event.preventDefault()
|
||||
callback(event)
|
||||
}
|
||||
}, 300)
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
onKeyDown.cancel?.()
|
||||
}
|
||||
}, [targetKey, callback])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener('change', onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener('change', onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { OAuth2Tokens } from 'arctic'
|
||||
import { decodeJwt } from 'jose'
|
||||
import { Authenticator } from 'remix-auth'
|
||||
import { CodeChallengeMethod, OAuth2Strategy } from 'remix-auth-oauth2'
|
||||
|
||||
export type User = {
|
||||
sub: string
|
||||
email: string
|
||||
name: string
|
||||
scope: string
|
||||
email_verified: boolean
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
export function createAuth(env: Env) {
|
||||
const authenticator = new Authenticator()
|
||||
const strategy = new OAuth2Strategy(
|
||||
{
|
||||
clientId: env.CLIENT_ID,
|
||||
clientSecret: env.CLIENT_SECRET,
|
||||
redirectURI: env.REDIRECT_URI,
|
||||
authorizationEndpoint: `${env.ISSUER_URL}/authorize`,
|
||||
tokenEndpoint: `${env.ISSUER_URL}/token`,
|
||||
tokenRevocationEndpoint: `${env.ISSUER_URL}/revoke`,
|
||||
scopes: env.SCOPE.split(' '),
|
||||
codeChallengeMethod: CodeChallengeMethod.S256
|
||||
},
|
||||
async ({ tokens }: { tokens: OAuth2Tokens }) => {
|
||||
const user = decodeJwt(tokens.idToken())
|
||||
|
||||
return {
|
||||
...user,
|
||||
accessToken: tokens.accessToken(),
|
||||
refreshToken: tokens.hasRefreshToken() ? tokens.refreshToken() : null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
authenticator.use(strategy, 'oidc')
|
||||
|
||||
return authenticator
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { requestIdContext, userContext } from '@/context'
|
||||
import type { User } from '@/lib/auth'
|
||||
import type { User } from '@repo/auth/auth'
|
||||
import { requestIdContext, userContext } from '@repo/auth/context'
|
||||
|
||||
import type { LoaderFunctionArgs } from 'react-router'
|
||||
|
||||
export enum HttpMethod {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { createCookieSessionStorage } from 'react-router'
|
||||
|
||||
export function createSessionStorage(env: Env) {
|
||||
const sessionStorage = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: '__session',
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
secrets: [env.SESSION_SECRET],
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 86400 * 7 // 7 days
|
||||
}
|
||||
})
|
||||
return sessionStorage
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { requestIdContext, userContext } from '@/context'
|
||||
import { createSessionStorage } from '@/lib/session'
|
||||
|
||||
import { createAuth, type User } from '@/lib/auth'
|
||||
import { decodeJwt } from 'jose'
|
||||
import { redirect, type LoaderFunctionArgs } from 'react-router'
|
||||
import type { OAuth2Strategy } from 'remix-auth-oauth2'
|
||||
|
||||
export const authMiddleware = async (
|
||||
{ request, context }: LoaderFunctionArgs,
|
||||
next: () => Promise<Response>
|
||||
): Promise<Response> => {
|
||||
const sessionStorage = createSessionStorage(context.cloudflare.env)
|
||||
const authenticator = createAuth(context.cloudflare.env)
|
||||
const strategy = authenticator.get<OAuth2Strategy<User>>('oidc')
|
||||
const session = await sessionStorage.getSession(request.headers.get('cookie'))
|
||||
const requestId = context.get(requestIdContext)
|
||||
let user = session.get('user') as User | null
|
||||
|
||||
session.set('returnTo', new URL(request.url).toString())
|
||||
|
||||
if (!user) {
|
||||
console.log('There is no user logged in')
|
||||
|
||||
return redirect('/login', {
|
||||
headers: new Headers({
|
||||
'Set-Cookie': await sessionStorage.commitSession(session)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const accessToken = decodeJwt(user.accessToken) as { exp: number }
|
||||
const accessTokenExp = accessToken.exp * 1000
|
||||
const leeway = 120 * 1000 // 2 minutes
|
||||
|
||||
if (Date.now() > accessTokenExp - leeway) {
|
||||
const tokens = await (strategy as any).refreshToken(user.refreshToken)
|
||||
|
||||
user = {
|
||||
...user,
|
||||
accessToken: tokens.accessToken(),
|
||||
refreshToken: tokens.refreshToken()
|
||||
}
|
||||
|
||||
console.debug(`[${requestId}] Refresh token retrieved`, user)
|
||||
// Should replace the user in the session
|
||||
session.set('user', user)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[${requestId}]`, error?.stack)
|
||||
|
||||
// If refreshing the token fails, remove the user from the current session
|
||||
// so the user is forced to sign in again
|
||||
session.unset('user')
|
||||
|
||||
return redirect('/login', {
|
||||
headers: new Headers({
|
||||
'Set-Cookie': await sessionStorage.commitSession(session)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
context.set(userContext, user)
|
||||
|
||||
const response = await next()
|
||||
const sessionCookie = await sessionStorage.commitSession(session)
|
||||
response.headers.set('Set-Cookie', sessionCookie)
|
||||
return response
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { requestIdContext } from '@/context'
|
||||
import { type LoaderFunctionArgs } from 'react-router'
|
||||
|
||||
export const loggingMiddleware = async (
|
||||
{ request, context }: LoaderFunctionArgs,
|
||||
next
|
||||
) => {
|
||||
const requestId = crypto.randomUUID()
|
||||
context.set(requestIdContext, requestId)
|
||||
|
||||
console.log(`[${requestId}] ${request.method} ${request.url}`)
|
||||
|
||||
const start = performance.now()
|
||||
const response = await next()
|
||||
const duration = performance.now() - start
|
||||
|
||||
console.log(`[${requestId}] Response ${response.status} (${duration}ms)`)
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
ScrollRestoration
|
||||
} from 'react-router'
|
||||
|
||||
import { loggingMiddleware } from '@repo/auth/middleware/logging'
|
||||
import { ThemeProvider } from '@repo/ui/components/theme-provider'
|
||||
import './app.css'
|
||||
import { loggingMiddleware } from './middleware/logging'
|
||||
|
||||
export const middleware: Route.MiddlewareFunction[] = [loggingMiddleware]
|
||||
|
||||
@@ -22,7 +22,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>saladeaula.digital</title>
|
||||
<title>admin.saladeaula.digital</title>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
|
||||
@@ -6,10 +6,11 @@ import { Suspense, useMemo } from 'react'
|
||||
import { Await, useSearchParams } from 'react-router'
|
||||
|
||||
import placeholder from '@/assets/placeholder.webp'
|
||||
import { SearchForm } from '@/components/search-form'
|
||||
import { Skeleton } from '@/components/skeleton'
|
||||
import { createSearch } from '@/lib/meili'
|
||||
import { request as req } from '@/lib/request'
|
||||
|
||||
import { SearchForm } from '@repo/ui/components/search-form'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
import {
|
||||
Card,
|
||||
CardFooter,
|
||||
|
||||
@@ -7,11 +7,12 @@ import { Suspense, useState } from 'react'
|
||||
import { Await, Link, useSearchParams } from 'react-router'
|
||||
|
||||
import { CustomizeColumns, DataTable } from '@/components/data-table'
|
||||
import { FacetedFilter } from '@/components/faceted-filter'
|
||||
import { RangeCalendarFilter } from '@/components/range-calendar-filter'
|
||||
import { SearchForm } from '@/components/search-form'
|
||||
import { Skeleton } from '@/components/skeleton'
|
||||
import { createSearch } from '@/lib/meili'
|
||||
|
||||
import { FacetedFilter } from '@repo/ui/components/faceted-filter'
|
||||
import { SearchForm } from '@repo/ui/components/search-form'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||
import {
|
||||
@@ -128,33 +129,6 @@ export default function Route({ loaderData: { data } }) {
|
||||
|
||||
<div className="flex gap-2.5 max-lg:flex-col w-full">
|
||||
<div className="flex gap-2.5 max-lg:flex-col">
|
||||
<FacetedFilter
|
||||
icon={BookCopyIcon}
|
||||
className="lg:flex-1"
|
||||
value={searchParams.getAll('courses')}
|
||||
onChange={(courses) => {
|
||||
setSearchParams((searchParams) => {
|
||||
searchParams.delete('courses')
|
||||
searchParams.delete('p')
|
||||
|
||||
if (statuses.length) {
|
||||
courses.forEach((s) =>
|
||||
searchParams.has('courses', s)
|
||||
? null
|
||||
: searchParams.append('courses', s)
|
||||
)
|
||||
}
|
||||
|
||||
return searchParams
|
||||
})
|
||||
}}
|
||||
title="Cursos"
|
||||
options={Object.entries(statuses).map(([key, value]) => ({
|
||||
value: key,
|
||||
...value
|
||||
}))}
|
||||
/>
|
||||
|
||||
<FacetedFilter
|
||||
icon={PlusCircleIcon}
|
||||
className="lg:flex-1"
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Suspense } from 'react'
|
||||
import { Await } from 'react-router'
|
||||
|
||||
import { DataTable } from '@/components/data-table'
|
||||
import { Skeleton } from '@/components/skeleton'
|
||||
import { createSearch } from '@/lib/meili'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
import { columns, type Order } from './columns'
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { Route } from './+types'
|
||||
import { Suspense } from 'react'
|
||||
import { Await } from 'react-router'
|
||||
|
||||
import { Skeleton } from '@/components/skeleton'
|
||||
import { request as req } from '@/lib/request'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
|
||||
export async function loader({ params, request, context }: Route.LoaderArgs) {
|
||||
const { id } = params
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Await } from 'react-router'
|
||||
|
||||
import type { Route } from './+types'
|
||||
|
||||
import { Skeleton } from '@/components/skeleton'
|
||||
import { request as req } from '@/lib/request'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export async function loader({ params, request, context }: Route.LoaderArgs) {
|
||||
|
||||
@@ -5,12 +5,13 @@ import { Suspense } from 'react'
|
||||
import { Await, Link, useSearchParams } from 'react-router'
|
||||
|
||||
import { DataTable } from '@/components/data-table'
|
||||
import { SearchForm } from '@/components/search-form'
|
||||
import { Skeleton } from '@/components/skeleton'
|
||||
import { createSearch } from '@/lib/meili'
|
||||
import { columns, type User } from './columns'
|
||||
|
||||
import { SearchForm } from '@repo/ui/components/search-form'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||
import { columns, type User } from './columns'
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
|
||||
@@ -3,11 +3,8 @@ import type { Route } from './+types'
|
||||
import { useLoaderData } from 'react-router'
|
||||
|
||||
import { DataTable } from '@/components/data-table'
|
||||
import { authMiddleware } from '@/middleware/auth'
|
||||
import { columns, type Webhook } from './columns'
|
||||
|
||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: 'Webhooks' }]
|
||||
}
|
||||
|
||||
@@ -3,17 +3,18 @@ import type { Route } from './+types'
|
||||
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
|
||||
|
||||
import { AppSidebar } from '@/components/app-sidebar'
|
||||
import { ModeToggle, ThemedImage } from '@/components/dark-mode'
|
||||
import { NavUser } from '@/components/nav-user'
|
||||
import { userContext } from '@/context'
|
||||
import { useIsMobile } from '@/hooks/use-mobile'
|
||||
import { request as req } from '@/lib/request'
|
||||
import { authMiddleware } from '@/middleware/auth'
|
||||
|
||||
import { userContext } from '@repo/auth/context'
|
||||
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
|
||||
import { NavUser } from '@repo/ui/components/nav-user'
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger
|
||||
} from '@repo/ui/components/ui/sidebar'
|
||||
import { useIsMobile } from '@repo/ui/hooks/use-mobile'
|
||||
|
||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { Route } from './+types'
|
||||
|
||||
import { redirect } from 'react-router'
|
||||
|
||||
import { userContext } from '@/context'
|
||||
import { request as req } from '@/lib/request'
|
||||
import { authMiddleware } from '@/middleware/auth'
|
||||
import { userContext } from '@repo/auth/context'
|
||||
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||
|
||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { Route } from './+types'
|
||||
|
||||
import { redirect } from 'react-router'
|
||||
|
||||
import { requestIdContext } from '@/context'
|
||||
import { createAuth, type User } from '@/lib/auth'
|
||||
import { createSessionStorage } from '@/lib/session'
|
||||
import { createAuth, type User } from '@repo/auth/auth'
|
||||
import { requestIdContext } from '@repo/auth/context'
|
||||
import { createSessionStorage } from '@repo/auth/session'
|
||||
|
||||
export async function loader({ request, context }: Route.ActionArgs) {
|
||||
const sessionStorage = createSessionStorage(context.cloudflare.env)
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { Route } from './+types'
|
||||
import { redirect } from 'react-router'
|
||||
import type { OAuth2Strategy } from 'remix-auth-oauth2'
|
||||
|
||||
import { createAuth, type User } from '@/lib/auth'
|
||||
import { createSessionStorage } from '@/lib/session'
|
||||
import { createAuth, type User } from '@repo/auth/auth'
|
||||
import { createSessionStorage } from '@repo/auth/session'
|
||||
|
||||
export async function loader({ request, context }: Route.LoaderArgs) {
|
||||
const authenticator = createAuth(context.cloudflare.env)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Route } from './+types'
|
||||
|
||||
import { userContext } from '@/context'
|
||||
import type { User } from '@/lib/auth'
|
||||
import { authMiddleware } from '@/middleware/auth'
|
||||
import type { User } from '@repo/auth/auth'
|
||||
import { userContext } from '@repo/auth/context'
|
||||
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||
|
||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||
export const loader = proxy
|
||||
|
||||
@@ -12,26 +12,20 @@
|
||||
"typecheck": "npm run cf-typegen && react-router typegen && tsc -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@brazilian-utils/brazilian-utils": "^1.0.0-rc.12",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@react-router/fs-routes": "^7.9.5",
|
||||
"@repo/ui": "*",
|
||||
"@repo/auth": "*",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"isbot": "^5.1.31",
|
||||
"jose": "^6.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.7.2",
|
||||
"meilisearch": "^0.54.0",
|
||||
"meilisearch-helper": "github:sergiors/meilisearch-helper",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-number-format": "^5.4.4",
|
||||
"react-router": "^7.9.5",
|
||||
"remix-auth-oauth2": "^3.4.1",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user