add download
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
|||||||
type Table,
|
type Table,
|
||||||
type VisibilityState
|
type VisibilityState
|
||||||
} from '@tanstack/react-table'
|
} from '@tanstack/react-table'
|
||||||
import { createContext, useState, type ReactNode } from 'react'
|
import { createContext, useEffect, useState, type ReactNode } from 'react'
|
||||||
import { useSearchParams } from 'react-router'
|
import { useSearchParams } from 'react-router'
|
||||||
|
|
||||||
import { Card, CardContent } from '@repo/ui/components/ui/card'
|
import { Card, CardContent } from '@repo/ui/components/ui/card'
|
||||||
@@ -28,6 +28,7 @@ interface DataTableProps<TData, TValue> {
|
|||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
columns: ColumnDef<TData, TValue>[]
|
columns: ColumnDef<TData, TValue>[]
|
||||||
data: TData[]
|
data: TData[]
|
||||||
|
onRowSelectionChange?: (rowSelection: TData[]) => void
|
||||||
pageIndex: number
|
pageIndex: number
|
||||||
sort: SortingState
|
sort: SortingState
|
||||||
pageSize: number
|
pageSize: number
|
||||||
@@ -45,6 +46,7 @@ export function DataTable<TData, TValue>({
|
|||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
rowCount,
|
rowCount,
|
||||||
|
onRowSelectionChange,
|
||||||
hiddenColumn = []
|
hiddenColumn = []
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
@@ -88,8 +90,6 @@ export function DataTable<TData, TValue>({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// table.getSelectedRowModel().flatRows.map((row) => row.original)
|
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
@@ -114,6 +114,11 @@ export function DataTable<TData, TValue>({
|
|||||||
onPaginationChange: setPagination
|
onPaginationChange: setPagination
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selected = table.getSelectedRowModel().flatRows.map((r) => r.original)
|
||||||
|
onRowSelectionChange?.(selected)
|
||||||
|
}, [rowSelection, table])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContext value={{ table }}>
|
<TableContext value={{ table }}>
|
||||||
<div className="space-y-2.5 max-md:mb-2">
|
<div className="space-y-2.5 max-md:mb-2">
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function RangeCalendarFilter({
|
|||||||
<Popover>
|
<Popover>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-9 border rounded-md bg-muted border-dashed flex items-center',
|
'h-9 border rounded-md bg-muted border-dashed flex items-center justify-center',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -130,9 +130,9 @@ export const columns: ColumnDef<Enrollment>[] = [
|
|||||||
{
|
{
|
||||||
accessorKey: 'created_at',
|
accessorKey: 'created_at',
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
<DataTableColumnHeader column={column} title="Matriculado em" />
|
<DataTableColumnHeader column={column} title="Cadastrado em" />
|
||||||
),
|
),
|
||||||
meta: { title: 'Matriculado em' },
|
meta: { title: 'Cadastrado em' },
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
enableHiding: true,
|
enableHiding: true,
|
||||||
cell: cellDate
|
cell: cellDate
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ export const statuses: Record<
|
|||||||
export const labels: Record<string, string> = {
|
export const labels: Record<string, string> = {
|
||||||
PENDING: 'Não iniciado',
|
PENDING: 'Não iniciado',
|
||||||
IN_PROGRESS: 'Em andamento',
|
IN_PROGRESS: 'Em andamento',
|
||||||
COMPLETED: 'Aprovado',
|
COMPLETED: 'Concluído',
|
||||||
FAILED: 'Reprovado',
|
FAILED: 'Reprovado',
|
||||||
CANCELED: 'Cancelado'
|
CANCELED: 'Cancelado'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sortings: Record<string, string> = {
|
export const sortings: Record<string, string> = {
|
||||||
created_at: 'Matriculado em',
|
created_at: 'Cadastrado em',
|
||||||
started_at: 'Iniciado em',
|
started_at: 'Iniciado em',
|
||||||
completed_at: 'Concluído em',
|
completed_at: 'Concluído em',
|
||||||
failed_at: 'Reprovado em',
|
failed_at: 'Reprovado em',
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
import { CalendarIcon, PlusCircleIcon, PlusIcon } from 'lucide-react'
|
import { flatten } from 'flat'
|
||||||
|
import {
|
||||||
|
CalendarIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
FileSpreadsheetIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
PlusCircleIcon,
|
||||||
|
PlusIcon,
|
||||||
|
TagIcon
|
||||||
|
} from 'lucide-react'
|
||||||
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
||||||
import { Suspense } from 'react'
|
import { Suspense, useState } from 'react'
|
||||||
import { Await, Link, useSearchParams } from 'react-router'
|
import { Await, Link, useSearchParams } from 'react-router'
|
||||||
|
import type { BookType } from 'xlsx'
|
||||||
|
import * as XLSX from 'xlsx'
|
||||||
|
|
||||||
import { DataTable, DataTableViewOptions } from '@/components/data-table'
|
import { DataTable, DataTableViewOptions } from '@/components/data-table'
|
||||||
import { RangeCalendarFilter } from '@/components/range-calendar-filter'
|
import { RangeCalendarFilter } from '@/components/range-calendar-filter'
|
||||||
@@ -13,6 +25,13 @@ import { FacetedFilter } from '@repo/ui/components/faceted-filter'
|
|||||||
import { SearchForm } from '@repo/ui/components/search-form'
|
import { SearchForm } from '@repo/ui/components/search-form'
|
||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from '@repo/ui/components/ui/dropdown-menu'
|
||||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
import { columns, type Enrollment } from './columns'
|
import { columns, type Enrollment } from './columns'
|
||||||
import { sortings, statuses } from './data'
|
import { sortings, statuses } from './data'
|
||||||
@@ -64,6 +83,7 @@ const formatted = new Intl.DateTimeFormat('en-CA', {
|
|||||||
|
|
||||||
export default function Route({ loaderData: { data } }) {
|
export default function Route({ loaderData: { data } }) {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const [rowSelection, setRowSelection] = useState<Enrollment[]>([])
|
||||||
const status = searchParams.get('status')
|
const status = searchParams.get('status')
|
||||||
const rangeParams = useRangeParams()
|
const rangeParams = useRangeParams()
|
||||||
|
|
||||||
@@ -87,6 +107,7 @@ export default function Route({ loaderData: { data } }) {
|
|||||||
data={hits as Enrollment[]}
|
data={hits as Enrollment[]}
|
||||||
pageIndex={page - 1}
|
pageIndex={page - 1}
|
||||||
pageSize={hitsPerPage}
|
pageSize={hitsPerPage}
|
||||||
|
onRowSelectionChange={setRowSelection}
|
||||||
rowCount={totalHits}
|
rowCount={totalHits}
|
||||||
hiddenColumn={[
|
hiddenColumn={[
|
||||||
'completed_at',
|
'completed_at',
|
||||||
@@ -95,97 +116,112 @@ export default function Route({ loaderData: { data } }) {
|
|||||||
'canceled_at'
|
'canceled_at'
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col 2xl:flex-row gap-2.5 mb-2.5">
|
<div className="flex gap-2.5 mb-2.5">
|
||||||
<div className="w-full 2xl:w-1/3">
|
{rowSelection.length ? (
|
||||||
<SearchForm
|
<>
|
||||||
defaultValue={searchParams.get('q') || ''}
|
<div className="flex gap-2.5 items-center">
|
||||||
placeholder={
|
<Button variant="outline">
|
||||||
<>
|
<TagIcon /> Marcador
|
||||||
Digite <Kbd className="border font-mono">/</Kbd> para
|
</Button>
|
||||||
pesquisar
|
<DropdownMenuExport rowSelection={rowSelection} />
|
||||||
</>
|
</div>
|
||||||
}
|
</>
|
||||||
onChange={(value) =>
|
) : (
|
||||||
setSearchParams((searchParams) => {
|
<>
|
||||||
searchParams.set('q', String(value))
|
<div className="w-full 2xl:w-1/3">
|
||||||
searchParams.delete('p')
|
<SearchForm
|
||||||
return searchParams
|
defaultValue={searchParams.get('q') || ''}
|
||||||
})
|
placeholder={
|
||||||
}
|
<>
|
||||||
/>
|
Digite <Kbd className="border font-mono">/</Kbd>{' '}
|
||||||
</div>
|
para pesquisar
|
||||||
|
</>
|
||||||
<div className="flex gap-2.5 max-lg:flex-col w-full">
|
}
|
||||||
<div className="flex gap-2.5 max-lg:flex-col">
|
onChange={(value) =>
|
||||||
<FacetedFilter
|
setSearchParams((searchParams) => {
|
||||||
title="Status"
|
searchParams.set('q', String(value))
|
||||||
icon={PlusCircleIcon}
|
searchParams.delete('p')
|
||||||
className="lg:flex-1"
|
|
||||||
value={status ? status.split(',') : []}
|
|
||||||
onChange={(statuses) => {
|
|
||||||
setSearchParams((searchParams) => {
|
|
||||||
searchParams.delete('status')
|
|
||||||
searchParams.delete('p')
|
|
||||||
|
|
||||||
if (statuses.length) {
|
|
||||||
searchParams.set('status', statuses.join(','))
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchParams
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
options={Object.entries(statuses).map(([key, value]) => ({
|
|
||||||
value: key,
|
|
||||||
...value
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RangeCalendarFilter
|
|
||||||
title="Período"
|
|
||||||
icon={CalendarIcon}
|
|
||||||
value={rangeParams}
|
|
||||||
className="lg:flex-1"
|
|
||||||
options={Object.entries(sortings).map(
|
|
||||||
([value, label]) => ({
|
|
||||||
value,
|
|
||||||
label
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
onChange={(props) => {
|
|
||||||
setSearchParams((searchParams) => {
|
|
||||||
if (!props) {
|
|
||||||
searchParams.delete('from')
|
|
||||||
searchParams.delete('to')
|
|
||||||
return searchParams
|
return searchParams
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
const { rangeField, dateRange } = props
|
<div className="flex gap-2.5 max-lg:flex-col w-full">
|
||||||
|
<div className="flex gap-2.5 max-lg:flex-col">
|
||||||
|
<FacetedFilter
|
||||||
|
title="Status"
|
||||||
|
icon={PlusCircleIcon}
|
||||||
|
className="lg:flex-1"
|
||||||
|
value={status ? status.split(',') : []}
|
||||||
|
onChange={(statuses) => {
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
searchParams.delete('status')
|
||||||
|
searchParams.delete('p')
|
||||||
|
|
||||||
searchParams.set(
|
if (statuses.length) {
|
||||||
'from',
|
searchParams.set('status', statuses.join(','))
|
||||||
`${rangeField}:${formatted.format(dateRange?.from)}`
|
}
|
||||||
)
|
|
||||||
searchParams.set(
|
|
||||||
'to',
|
|
||||||
formatted.format(dateRange?.to)
|
|
||||||
)
|
|
||||||
|
|
||||||
return searchParams
|
return searchParams
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
options={Object.entries(statuses).map(
|
||||||
</div>
|
([key, value]) => ({
|
||||||
|
value: key,
|
||||||
|
...value
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="lg:ml-auto flex gap-2.5">
|
<RangeCalendarFilter
|
||||||
<DataTableViewOptions className="flex-1" />
|
title="Período"
|
||||||
|
icon={CalendarIcon}
|
||||||
|
value={rangeParams}
|
||||||
|
className="lg:flex-1"
|
||||||
|
options={Object.entries(sortings).map(
|
||||||
|
([value, label]) => ({
|
||||||
|
value,
|
||||||
|
label
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
onChange={(props) => {
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
if (!props) {
|
||||||
|
searchParams.delete('from')
|
||||||
|
searchParams.delete('to')
|
||||||
|
return searchParams
|
||||||
|
}
|
||||||
|
|
||||||
<Button className="flex-1" asChild>
|
const { rangeField, dateRange } = props
|
||||||
<Link to="add">
|
|
||||||
<PlusIcon /> Adicionar
|
searchParams.set(
|
||||||
</Link>
|
'from',
|
||||||
</Button>
|
`${rangeField}:${formatted.format(dateRange?.from)}`
|
||||||
</div>
|
)
|
||||||
</div>
|
searchParams.set(
|
||||||
|
'to',
|
||||||
|
formatted.format(dateRange?.to)
|
||||||
|
)
|
||||||
|
|
||||||
|
return searchParams
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:ml-auto flex gap-2.5">
|
||||||
|
<DataTableViewOptions className="flex-1" />
|
||||||
|
|
||||||
|
<Button className="flex-1" asChild>
|
||||||
|
<Link to="add">
|
||||||
|
<PlusIcon /> Adicionar
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
)
|
)
|
||||||
@@ -213,3 +249,67 @@ function useRangeParams() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function DropdownMenuExport({
|
||||||
|
rowSelection = []
|
||||||
|
}: {
|
||||||
|
rowSelection: object[]
|
||||||
|
}) {
|
||||||
|
const headers = {
|
||||||
|
id: 'ID',
|
||||||
|
'user.name': 'Nome',
|
||||||
|
'user.email': 'Email',
|
||||||
|
'user.cpf': 'CPF',
|
||||||
|
'course.name': 'Curso',
|
||||||
|
status: 'Status',
|
||||||
|
progress: 'Progresso',
|
||||||
|
created_at: 'Cadastrado em',
|
||||||
|
started_at: 'Iniciado em',
|
||||||
|
completed_at: 'Concluído em',
|
||||||
|
failed_at: 'Reprovado em',
|
||||||
|
canceled_at: 'Cancelado em'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExport = (bookType: BookType) => () => {
|
||||||
|
if (!rowSelection.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = Object.keys(headers)
|
||||||
|
const data = rowSelection.map((data) => {
|
||||||
|
const obj: Record<string, string> = flatten(data)
|
||||||
|
return Object.fromEntries(header.map((k) => [k, obj?.[k]]))
|
||||||
|
})
|
||||||
|
const workbook = XLSX.utils.book_new()
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(data, { header })
|
||||||
|
|
||||||
|
XLSX.utils.sheet_add_aoa(worksheet, [Object.values(headers)], {
|
||||||
|
origin: 'A1'
|
||||||
|
})
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
|
||||||
|
XLSX.writeFile(workbook, `${formatted.format(new Date())}.${bookType}`, {
|
||||||
|
bookType,
|
||||||
|
compression: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" className="cursor-pointer">
|
||||||
|
<DownloadIcon /> Baixar <ChevronDownIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-52" align="start">
|
||||||
|
<DropdownMenuGroup className="*:cursor-pointer">
|
||||||
|
<DropdownMenuItem onClick={handleExport('xlsx')}>
|
||||||
|
<FileSpreadsheetIcon /> Microsoft Excel (.xlsx)
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleExport('csv')}>
|
||||||
|
<FileTextIcon /> CSV (.csv)
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"file-saver": "^2.0.5",
|
"flat": "^6.0.1",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/vite-plugin": "^1.13.18",
|
"@cloudflare/vite-plugin": "^1.13.18",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/luxon": "^3.7.1",
|
"@types/luxon": "^3.7.1",
|
||||||
"@types/node": "^24.9.2",
|
"@types/node": "^24.9.2",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Resources:
|
|||||||
Type: AWS::Serverless::HttpApi
|
Type: AWS::Serverless::HttpApi
|
||||||
Properties:
|
Properties:
|
||||||
CorsConfiguration:
|
CorsConfiguration:
|
||||||
AllowOrigins: ["*"]
|
AllowOrigins: ['*']
|
||||||
AllowMethods: [GET, POST, OPTIONS]
|
AllowMethods: [GET, POST, OPTIONS]
|
||||||
AllowHeaders: [Content-Type, X-Requested-With, Authorization]
|
AllowHeaders: [Content-Type, X-Requested-With, Authorization]
|
||||||
|
|
||||||
@@ -99,55 +99,12 @@ Resources:
|
|||||||
Method: GET
|
Method: GET
|
||||||
ApiId: !Ref HttpApi
|
ApiId: !Ref HttpApi
|
||||||
|
|
||||||
OIDCDistribution:
|
|
||||||
Type: AWS::CloudFront::Distribution
|
|
||||||
Properties:
|
|
||||||
DistributionConfig:
|
|
||||||
Enabled: true
|
|
||||||
Origins:
|
|
||||||
- Id: OidcApiOrigin
|
|
||||||
DomainName: !Sub "${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}"
|
|
||||||
CustomOriginConfig:
|
|
||||||
OriginProtocolPolicy: https-only
|
|
||||||
DefaultCacheBehavior:
|
|
||||||
TargetOriginId: OidcApiOrigin
|
|
||||||
ViewerProtocolPolicy: redirect-to-https
|
|
||||||
AllowedMethods: [GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE]
|
|
||||||
CachedMethods: [GET, HEAD]
|
|
||||||
ForwardedValues:
|
|
||||||
QueryString: true
|
|
||||||
Headers:
|
|
||||||
- Origin
|
|
||||||
- Access-Control-Request-Method
|
|
||||||
- Access-Control-Request-Headers
|
|
||||||
Cookies:
|
|
||||||
Forward: all
|
|
||||||
DefaultTTL: 0
|
|
||||||
MinTTL: 0
|
|
||||||
MaxTTL: 0
|
|
||||||
CacheBehaviors:
|
|
||||||
- PathPattern: "/.well-known/*"
|
|
||||||
TargetOriginId: OidcApiOrigin
|
|
||||||
ViewerProtocolPolicy: redirect-to-https
|
|
||||||
AllowedMethods: [GET, HEAD, OPTIONS]
|
|
||||||
CachedMethods: [GET, HEAD, OPTIONS]
|
|
||||||
ForwardedValues:
|
|
||||||
QueryString: false
|
|
||||||
Headers:
|
|
||||||
- Origin
|
|
||||||
DefaultTTL: 3600 # 1 hour
|
|
||||||
MinTTL: 300 # 5 minutes
|
|
||||||
MaxTTL: 86400 # 1 day
|
|
||||||
|
|
||||||
Outputs:
|
Outputs:
|
||||||
HttpApiUrl:
|
HttpApiUrl:
|
||||||
Description: URL of your API endpoint
|
Description: URL of your API endpoint
|
||||||
Value:
|
Value:
|
||||||
Fn::Sub: "https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}"
|
Fn::Sub: 'https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}'
|
||||||
HttpApiId:
|
HttpApiId:
|
||||||
Description: Api ID of HttpApi
|
Description: Api ID of HttpApi
|
||||||
Value:
|
Value:
|
||||||
Ref: HttpApi
|
Ref: HttpApi
|
||||||
OIDCDistributionDomain:
|
|
||||||
Description: Domain of CloudFront Distribution domain for OIDC endpoints
|
|
||||||
Value: !GetAtt OIDCDistribution.DomainName
|
|
||||||
|
|||||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -31,7 +31,7 @@
|
|||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"file-saver": "^2.0.5",
|
"flat": "^6.0.1",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/vite-plugin": "^1.13.18",
|
"@cloudflare/vite-plugin": "^1.13.18",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/luxon": "^3.7.1",
|
"@types/luxon": "^3.7.1",
|
||||||
"@types/node": "^24.9.2",
|
"@types/node": "^24.9.2",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
@@ -4680,6 +4681,13 @@
|
|||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/file-saver": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/js-cookie": {
|
"node_modules/@types/js-cookie": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
|
||||||
@@ -5324,11 +5332,17 @@
|
|||||||
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
|
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/file-saver": {
|
"node_modules/flat": {
|
||||||
"version": "2.0.5",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/flat/-/flat-6.0.1.tgz",
|
||||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
|
"integrity": "sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==",
|
||||||
"license": "MIT"
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"flat": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user