update
This commit is contained in:
@@ -75,11 +75,15 @@ export function DataTable<TData, TValue>({
|
||||
)
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
useState<VisibilityState>(hiddenColumn_)
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
rowSelection,
|
||||
columnVisibility,
|
||||
pagination: {
|
||||
pageIndex,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useSidebar
|
||||
} from '@repo/ui/components/ui/sidebar'
|
||||
import { useIsMobile } from '@repo/ui/hooks/use-mobile'
|
||||
|
||||
import { type LucideIcon } from 'lucide-react'
|
||||
import { NavLink, useParams } from 'react-router'
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
SidebarRail,
|
||||
useSidebar
|
||||
} from '@repo/ui/components/ui/sidebar'
|
||||
|
||||
import { initials } from '@repo/ui/lib/utils'
|
||||
|
||||
type Org = {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { User } from '@/lib/auth'
|
||||
import { createContext } from 'react-router'
|
||||
|
||||
export const userContext = createContext<User | null>(null)
|
||||
export const requestIdContext = createContext<string | null>(null)
|
||||
@@ -92,8 +92,8 @@ export default function Route({ loaderData: { data } }) {
|
||||
<SearchForm
|
||||
placeholder={
|
||||
<>
|
||||
Pressione <Kbd className="border font-mono">/</Kbd> para
|
||||
filtrar...
|
||||
Digite <Kbd className="border font-mono">/</Kbd> para
|
||||
pesquisar
|
||||
</>
|
||||
}
|
||||
defaultValue={term}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
|
||||
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
||||
import { Badge } from '@repo/ui/components/ui/badge'
|
||||
import { Checkbox } from '@repo/ui/components/ui/checkbox'
|
||||
import { Progress } from '@repo/ui/components/ui/progress'
|
||||
import { cn, initials } from '@repo/ui/lib/utils'
|
||||
|
||||
@@ -83,6 +84,26 @@ const statusTranslate: Record<string, string> = {
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<Enrollment>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Selecionar tudo"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Selecionar linha"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Colaborador',
|
||||
enableHiding: false,
|
||||
@@ -145,30 +166,35 @@ export const columns: ColumnDef<Enrollment>[] = [
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Matriculado em',
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
cell: cellDate
|
||||
},
|
||||
{
|
||||
accessorKey: 'started_at',
|
||||
header: 'Iniciado em',
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
cell: cellDate
|
||||
},
|
||||
{
|
||||
accessorKey: 'completed_at',
|
||||
header: 'Aprovado em',
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
cell: cellDate
|
||||
},
|
||||
{
|
||||
accessorKey: 'failed_at',
|
||||
header: 'Reprovado em',
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
cell: cellDate
|
||||
},
|
||||
{
|
||||
accessorKey: 'canceled_at',
|
||||
header: 'Cancelado em',
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
cell: cellDate
|
||||
}
|
||||
|
||||
@@ -113,8 +113,8 @@ export default function Route({ loaderData: { data } }) {
|
||||
defaultValue={searchParams.get('q') || ''}
|
||||
placeholder={
|
||||
<>
|
||||
Pressione <Kbd className="border font-mono">/</Kbd> para
|
||||
pesquisar...
|
||||
Digite <Kbd className="border font-mono">/</Kbd> para
|
||||
pesquisar
|
||||
</>
|
||||
}
|
||||
onChange={(value) =>
|
||||
|
||||
@@ -72,8 +72,8 @@ export default function Route({ loaderData: { data } }) {
|
||||
<SearchForm
|
||||
placeholder={
|
||||
<>
|
||||
Pressione <Kbd className="border font-mono">/</Kbd> para
|
||||
pesquisar...
|
||||
Digite <Kbd className="border font-mono">/</Kbd> para
|
||||
pesquisar
|
||||
</>
|
||||
}
|
||||
defaultValue={searchParams.get('q') || ''}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { Route } from './+types'
|
||||
|
||||
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
|
||||
import {
|
||||
createCookie,
|
||||
Outlet,
|
||||
type ShouldRevalidateFunctionArgs
|
||||
} from 'react-router'
|
||||
|
||||
import { AppSidebar } from '@/components/app-sidebar'
|
||||
import { request as req } from '@/lib/request'
|
||||
@@ -14,13 +18,14 @@ import {
|
||||
SidebarProvider,
|
||||
SidebarTrigger
|
||||
} from '@repo/ui/components/ui/sidebar'
|
||||
import { useIsMobile } from '@repo/ui/hooks/use-mobile'
|
||||
|
||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||
|
||||
export async function loader({ params, context, request }: Route.ActionArgs) {
|
||||
const rawCookie = request.headers.get('cookie')
|
||||
const user = context.get(userContext)
|
||||
|
||||
const sidebarState = await createCookie('sidebar_state').parse(rawCookie)
|
||||
const r = await req({
|
||||
url: `/users/${user.sub}/orgs?limit=25`,
|
||||
request,
|
||||
@@ -39,7 +44,7 @@ export async function loader({ params, context, request }: Route.ActionArgs) {
|
||||
const exists = orgs.some(({ id }) => id === params.orgid)
|
||||
|
||||
if (exists) {
|
||||
return { user, orgs }
|
||||
return { user, orgs, sidebarState }
|
||||
}
|
||||
|
||||
throw new Response(null, { status: 401 })
|
||||
@@ -52,12 +57,12 @@ export function shouldRevalidate({
|
||||
return currentParams.orgid !== nextParams.orgid
|
||||
}
|
||||
|
||||
export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||
const { user, orgs } = loaderData
|
||||
const isMobile = useIsMobile()
|
||||
export default function Route({ loaderData }: Route.ComponentProps) {
|
||||
const { user, orgs, sidebarState } = loaderData
|
||||
console.log(sidebarState)
|
||||
|
||||
return (
|
||||
<SidebarProvider className="flex">
|
||||
<SidebarProvider defaultOpen={sidebarState === 'true'} className="flex">
|
||||
<AppSidebar orgs={orgs} />
|
||||
|
||||
<SidebarInset className="relative flex flex-col flex-1 min-w-0">
|
||||
@@ -66,7 +71,8 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||
px-4 py-2 lg:py-4 sticky top-0 z-5"
|
||||
>
|
||||
<div className="container mx-auto flex items-center">
|
||||
{isMobile ? <SidebarTrigger /> : <ThemedImage />}
|
||||
<SidebarTrigger className="md:hidden" />
|
||||
<ThemedImage className="max-md:hidden" />
|
||||
|
||||
<div className="ml-auto flex gap-2.5 items-center">
|
||||
<ModeToggle />
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-router/fs-routes": "^7.9.5",
|
||||
"@repo/ui": "*",
|
||||
"@repo/auth": "*",
|
||||
"@repo/ui": "*",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useKeyPress } from '@/hooks/use-keypress'
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput
|
||||
} from '@repo/ui/components/ui/input-group'
|
||||
import { useKeyPress } from '@/hooks/use-keypress'
|
||||
import clsx from 'clsx'
|
||||
import { debounce } from 'lodash'
|
||||
import { SearchIcon } from 'lucide-react'
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { throttle } from 'lodash'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export function useKeyPress(targetKey, callback) {
|
||||
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,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,4 +1,4 @@
|
||||
import { userContext } from '@/context'
|
||||
import { userContext } from '@repo/auth/context'
|
||||
import type { LoaderFunctionArgs } from 'react-router'
|
||||
|
||||
enum Method {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
export default [
|
||||
layout('routes/layout.tsx', [
|
||||
index('routes/index.tsx'),
|
||||
route('certs', 'routes/certs.tsx'),
|
||||
route('payments', 'routes/payments.tsx'),
|
||||
route('settings', 'routes/settings.tsx'),
|
||||
route('player/:course', 'routes/player.tsx'),
|
||||
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
} from '@repo/ui/components/ui/empty'
|
||||
import {
|
||||
BanIcon,
|
||||
BookCopyIcon,
|
||||
CircleCheckIcon,
|
||||
CircleIcon,
|
||||
CircleOffIcon,
|
||||
CirclePlusIcon,
|
||||
CircleXIcon,
|
||||
TimerIcon,
|
||||
type LucideIcon
|
||||
@@ -131,7 +131,7 @@ export default function Component({
|
||||
/>
|
||||
</div>
|
||||
<FacetedFilter
|
||||
icon={BookCopyIcon}
|
||||
icon={CirclePlusIcon}
|
||||
value={searchParams.getAll('status')}
|
||||
onChange={(statuses) => {
|
||||
setSearchParams((searchParams) => {
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import type { Route } from './+types'
|
||||
|
||||
import { Outlet } from 'react-router'
|
||||
import { Link, NavLink, Outlet } from 'react-router'
|
||||
|
||||
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 {
|
||||
NavigationMenu,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList
|
||||
} from '@repo/ui/components/ui/navigation-menu'
|
||||
import { useIsMobile } from '@repo/ui/hooks/use-mobile'
|
||||
|
||||
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
|
||||
|
||||
@@ -16,6 +22,7 @@ export async function loader({ context }: Route.ActionArgs) {
|
||||
}
|
||||
|
||||
export default function Component({ loaderData }: Route.ComponentProps) {
|
||||
const isMobile = useIsMobile()
|
||||
const { user } = loaderData
|
||||
|
||||
return (
|
||||
@@ -25,7 +32,25 @@ export default function Component({ loaderData }: Route.ComponentProps) {
|
||||
px-4 py-2 lg:py-4 sticky top-0 z-5"
|
||||
>
|
||||
<div className="container mx-auto flex items-center">
|
||||
<ThemedImage />
|
||||
<div className="flex gap-5">
|
||||
<Link to="/">
|
||||
<ThemedImage />
|
||||
</Link>
|
||||
|
||||
<NavigationMenu viewport={isMobile}>
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<NavMenuLink to="/">Meus cursos</NavMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavMenuLink to="/certs">Certificados</NavMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavMenuLink to="/payments">Histórico de compras</NavMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex gap-2.5 items-center">
|
||||
<ModeToggle />
|
||||
@@ -42,3 +67,14 @@ export default function Component({ loaderData }: Route.ComponentProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NavMenuLink({ children, ...props }) {
|
||||
return (
|
||||
<NavigationMenuLink
|
||||
className="font-medium aria-[current=page]:bg-muted"
|
||||
asChild
|
||||
>
|
||||
<NavLink {...props}>{children}</NavLink>
|
||||
</NavigationMenuLink>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ import type { Route } from './+types'
|
||||
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import { userContext } from '@/context'
|
||||
import { request as req } from '@/lib/request'
|
||||
import type { User } from '@/middleware/auth'
|
||||
|
||||
import type { User } from '@repo/auth/auth'
|
||||
import { userContext } from '@repo/auth/context'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
|
||||
Reference in New Issue
Block a user