Compare commits

..

8 Commits

17 changed files with 468 additions and 182 deletions

View File

@ -29,19 +29,27 @@ const dataResponseSchema = z.object({
})
export type TNewsResponse = z.infer<typeof newsResponseSchema>
export type TNewsResponseData = z.infer<typeof dataResponseSchema>
export type TAuthorResponse = z.infer<typeof authorSchema>
type TParameters = {
categories?: string[]
tags?: string[]
active?: boolean
limit?: number
page?: number
} & THttpServer
export const getNews = async (parameters?: TParameters) => {
const { categories, tags, ...restParameters } = parameters || {}
const { categories, tags, active, limit, page, ...restParameters } =
parameters || {}
try {
const { data } = await HttpServer(restParameters).get(`/api/news`, {
params: {
...(categories && { categories: categories.join('+') }),
...(tags && { tags: tags.join('+') }),
...(active && { active }),
...(limit && { limit }),
...(page && { page }),
},
})
return dataResponseSchema.parse(data)

View File

@ -0,0 +1,10 @@
import { useAsyncError } from 'react-router'
export const ErrorAwait = () => {
const error = useAsyncError()
return (
<p>
{error instanceof Error ? error.message : 'An unexpected error occurred.'}
</p>
)
}

View File

@ -1,15 +1,17 @@
import useEmblaCarousel from 'embla-carousel-react'
import { useCallback, useEffect, useState } from 'react'
import { useRouteLoaderData } from 'react-router'
import { Suspense, useCallback, useEffect, useState } from 'react'
import { Await, useRouteLoaderData } from 'react-router'
import { stripHtml } from 'string-strip-html'
import { Button } from '~/components/ui/button'
import { ErrorAwait } from '~/components/error/await'
import { CarouselButton } from '~/components/ui/button-slide'
import { useNewsContext } from '~/contexts/news'
import type { loader } from '~/routes/_news'
import type { TNews } from '~/types/news'
import { getPremiumAttribute } from '~/utils/render'
import { Button } from './button'
export const CarouselHero = (properties: TNews) => {
const { setIsSuccessOpen } = useNewsContext()
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
@ -72,43 +74,55 @@ export const CarouselHero = (properties: TNews) => {
ref={emblaReference}
>
<div className="embla__container hero flex sm:gap-x-8">
{items.map(
({ featured_image, title, content, slug, is_premium }, index) => (
<div
className="embla__slide hero w-full min-w-0 flex-none"
key={index}
>
<div className="max-sm:mt-2 sm:flex">
<img
className="col-span-2 aspect-[174/100] object-cover"
src={featured_image}
alt={title}
/>
<div className="flex h-full flex-col justify-between gap-7 sm:px-5">
<div>
<h3 className="mt-2 w-full text-2xl font-bold sm:mt-0 sm:text-4xl">
{title}
</h3>
<p className="text-md mt-5 line-clamp-10 text-[#777777] sm:text-xl">
{stripHtml(content).result}
</p>
</div>
<Button
size="block"
{...getPremiumAttribute({
isPremium: is_premium,
slug,
onClick: () => setIsSuccessOpen('warning'),
userData,
})}
<Suspense fallback={<div>Loading...</div>}>
<Await
resolve={items}
errorElement={<ErrorAwait />}
>
{(value) =>
value.data.map(
(
{ featured_image, title, content, slug, is_premium },
index,
) => (
<div
className="embla__slide hero w-full min-w-0 flex-none"
key={index}
>
View More
</Button>
</div>
</div>
</div>
),
)}
<div className="max-sm:mt-2 sm:flex">
<img
className="col-span-2 aspect-[174/100] object-cover"
src={featured_image}
alt={title}
/>
<div className="flex h-full flex-col justify-between gap-7 sm:px-5">
<div>
<h3 className="mt-2 w-full text-2xl font-bold sm:mt-0 sm:text-4xl">
{title}
</h3>
<p className="text-md mt-5 line-clamp-10 text-[#777777] sm:text-xl">
{stripHtml(content).result}
</p>
</div>
<Button
size="block"
{...getPremiumAttribute({
isPremium: is_premium,
slug,
onClick: () => setIsSuccessOpen('warning'),
userData,
})}
>
View More
</Button>
</div>
</div>
</div>
),
)
}
</Await>
</Suspense>
</div>
</div>
</div>

View File

@ -1,15 +1,16 @@
import useEmblaCarousel from 'embla-carousel-react'
import { useCallback, useEffect, useState } from 'react'
import { useRouteLoaderData } from 'react-router'
import { Suspense, useCallback, useEffect, useState } from 'react'
import { Await, useRouteLoaderData } from 'react-router'
import { stripHtml } from 'string-strip-html'
import { Button } from '~/components/ui/button'
import { ErrorAwait } from '~/components/error/await'
import { CarouselButton } from '~/components/ui/button-slide'
import { useNewsContext } from '~/contexts/news'
import type { loader } from '~/routes/_news'
import type { TNews } from '~/types/news'
import { getPremiumAttribute } from '~/utils/render'
import { Button } from './button'
import { Tags } from './tags'
export const CarouselSection = (properties: TNews) => {
@ -79,50 +80,59 @@ export const CarouselSection = (properties: TNews) => {
ref={emblaReference}
>
<div className="embla__container col-span-3 flex max-h-[586px] sm:gap-x-8">
{items.map(
(
{ featured_image, title, content, tags, slug, is_premium },
index,
) => (
<div
className="embla__slide w-full min-w-0 flex-none sm:w-1/3"
key={index}
>
<div className="flex flex-col justify-between gap-3">
<img
className="aspect-[174/100] max-h-[280px] w-full rounded-md object-cover sm:aspect-[5/4]"
src={featured_image}
alt={title}
/>
<div className={'flex flex-col justify-between gap-4'}>
<div className="flex h-28 flex-col items-start justify-center gap-4">
<Tags
tags={tags || []}
is_premium={is_premium}
/>
<h3 className="mt-2 line-clamp-2 w-full text-xl font-bold sm:text-2xl lg:mt-0">
{title}
</h3>
</div>
<p className="text-md line-clamp-3 text-[#777777] sm:text-xl">
{stripHtml(content).result}
</p>
<Button
size="block"
{...getPremiumAttribute({
isPremium: is_premium,
slug,
onClick: () => setIsSuccessOpen('warning'),
userData,
})}
<Suspense fallback={<div>Loading...</div>}>
<Await
resolve={items}
errorElement={<ErrorAwait />}
>
{(value) =>
value.data.map(
(
{ featured_image, title, content, tags, slug, is_premium },
index,
) => (
<div
className="embla__slide w-full min-w-0 flex-none sm:w-1/3"
key={index}
>
View More
</Button>
</div>
</div>
</div>
),
)}
<div className="flex flex-col justify-between gap-3">
<img
className="aspect-[174/100] max-h-[280px] w-full rounded-md object-cover sm:aspect-[5/4]"
src={featured_image}
alt={title}
/>
<div className={'flex flex-col justify-between gap-4'}>
<div className="flex h-28 flex-col items-start justify-center gap-4">
<Tags
tags={tags || []}
is_premium={is_premium}
/>
<h3 className="mt-2 line-clamp-2 w-full text-xl font-bold sm:text-2xl lg:mt-0">
{title}
</h3>
</div>
<p className="text-md line-clamp-3 text-[#777777] sm:text-xl">
{stripHtml(content).result}
</p>
<Button
size="block"
{...getPremiumAttribute({
isPremium: is_premium,
slug,
onClick: () => setIsSuccessOpen('warning'),
userData,
})}
>
View More
</Button>
</div>
</div>
</div>
),
)
}
</Await>
</Suspense>
</div>
</div>
</div>

View File

@ -1,23 +1,19 @@
import { useRouteLoaderData } from 'react-router'
import { Suspense } from 'react'
import { Await, useRouteLoaderData } from 'react-router'
import { stripHtml } from 'string-strip-html'
import { twMerge } from 'tailwind-merge'
import type { TNewsResponse } from '~/apis/common/get-news'
import { ErrorAwait } from '~/components/error/await'
import { CarouselNextIcon } from '~/components/icons/carousel-next'
import { CarouselPreviousIcon } from '~/components/icons/carousel-previous'
import { Button } from '~/components/ui/button'
import { useNewsContext } from '~/contexts/news'
import type { loader } from '~/routes/_news'
import type { TNews } from '~/types/news'
import { getPremiumAttribute } from '~/utils/render'
import { Tags } from './tags'
type TNews = {
title: string
description: string
items: TNewsResponse[]
}
export const CategorySection = (properties: TNews) => {
const { setIsSuccessOpen } = useNewsContext()
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
@ -47,56 +43,67 @@ export const CategorySection = (properties: TNews) => {
</div>
<div className="grid sm:grid-cols-3 sm:gap-x-8">
{items.map(
(
{ featured_image, title, content, tags, slug, is_premium },
index,
) => (
<div
key={index}
className={twMerge('grid gap-3 sm:gap-x-8')}
>
<img
className={twMerge(
'aspect-[174/100] w-full rounded-md object-cover sm:aspect-[5/4]',
)}
src={featured_image}
alt={title}
/>
<div className={twMerge('flex flex-col justify-between gap-4')}>
<Tags
tags={tags}
is_premium={is_premium}
/>
<div>
<h3
className={twMerge(
'mt-2 w-full text-xl font-bold sm:mt-0 sm:text-2xl',
)}
<Suspense fallback={<div>Loading...</div>}>
<Await
resolve={items}
errorElement={<ErrorAwait />}
>
{(value) =>
value.data.map(
(
{ featured_image, title, content, tags, slug, is_premium },
index,
) => (
<div
key={index}
className={twMerge('grid gap-3 sm:gap-x-8')}
>
{title}
</h3>
<p className="text-md mt-5 line-clamp-3 text-[#777777] sm:text-xl">
{stripHtml(content).result}
</p>
</div>
<Button
size="block"
{...getPremiumAttribute({
isPremium: is_premium,
slug,
onClick: () => setIsSuccessOpen('warning'),
userData,
})}
className="mb-5"
>
View More
</Button>
</div>
</div>
),
)}
<img
className={twMerge(
'aspect-[174/100] w-full rounded-md object-cover sm:aspect-[5/4]',
)}
src={featured_image}
alt={title}
/>
<div
className={twMerge('flex flex-col justify-between gap-4')}
>
<Tags
tags={tags}
is_premium={is_premium}
/>
<div>
<h3
className={twMerge(
'mt-2 w-full text-xl font-bold sm:mt-0 sm:text-2xl',
)}
>
{title}
</h3>
<p className="text-md mt-5 line-clamp-3 text-[#777777] sm:text-xl">
{stripHtml(content).result}
</p>
</div>
<Button
size="block"
{...getPremiumAttribute({
isPremium: is_premium,
slug,
onClick: () => setIsSuccessOpen('warning'),
userData,
})}
className="mb-5"
>
View More
</Button>
</div>
</div>
),
)
}
</Await>
</Suspense>
</div>
<div className="my-5 mt-5 flex flex-row-reverse">

View File

@ -2,10 +2,14 @@ import xior, { merge } from 'xior'
const baseURL = import.meta.env.VITE_API_URL
export type THttpServer = { accessToken?: string }
export type THttpServer = {
accessToken?: string
ipAddress?: string | null
userAgent?: string | null
}
export const HttpServer = (parameters?: THttpServer) => {
const { accessToken } = parameters || {}
const { accessToken, ipAddress, userAgent } = parameters || {}
const instance = xior.create({
baseURL,
})
@ -16,6 +20,8 @@ export const HttpServer = (parameters?: THttpServer) => {
return merge(config, {
headers: {
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
...(ipAddress && { 'X-Ip-Address': ipAddress }),
...(userAgent && { 'X-User-Agent': userAgent }),
},
})
})

View File

@ -17,7 +17,7 @@ export const NewsCategoriesPage = () => {
<CategorySection
title={name || ''}
description={description || ''}
items={newsData || []}
items={newsData || Promise.resolve({ data: [] })}
/>
</Card>
</div>

View File

@ -21,7 +21,7 @@ export const NewsDetailPage = () => {
const berita: TNews = {
title: loaderData?.beritaCategory?.name || '',
description: loaderData?.beritaCategory?.description || '',
items: loaderData?.beritaNews || [],
items: loaderData?.beritaData || Promise.resolve({ data: [] }),
}
const currentUrl = globalThis.location
const { title, content, featured_image, author, live_at, tags } =

View File

@ -12,17 +12,17 @@ export const NewsPage = () => {
const spotlight: TNews = {
title: loaderData?.spotlightCategory?.name || '',
description: loaderData?.spotlightCategory?.description || '',
items: loaderData?.spotlightNews || [],
items: loaderData?.spotlightData || Promise.resolve({ data: [] }),
}
const berita: TNews = {
title: loaderData?.beritaCategory?.name || '',
description: loaderData?.beritaCategory?.description || '',
items: loaderData?.beritaNews || [],
items: loaderData?.beritaData || Promise.resolve({ data: [] }),
}
const kajian: TNews = {
title: loaderData?.kajianCategory?.name || '',
description: loaderData?.kajianCategory?.description || '',
items: loaderData?.kajianNews || [],
items: loaderData?.kajianData || Promise.resolve({ data: [] }),
}
return (

View File

@ -25,31 +25,25 @@ export const loader = async ({}: Route.LoaderArgs) => {
(category) => category.code === kajianCode,
)
let { data: spotlightNews } = await getNews({
const spotlightData = getNews({
categories: [spotlightCode],
active: true,
})
spotlightNews = spotlightNews.filter(
(news) => new Date(news.live_at) <= new Date(),
)
let { data: beritaNews } = await getNews({
const beritaData = getNews({
categories: [beritaCode],
active: true,
})
beritaNews = beritaNews.filter((news) => new Date(news.live_at) <= new Date())
let { data: kajianNews } = await getNews({
const kajianData = getNews({
categories: [kajianCode],
active: true,
})
kajianNews = kajianNews.filter((news) => new Date(news.live_at) <= new Date())
return {
spotlightCategory,
spotlightCode,
beritaCategory,
beritaCode,
kajianCategory,
kajianCode,
spotlightNews,
beritaNews,
kajianNews,
spotlightData,
beritaData,
kajianData,
}
}

View File

@ -11,8 +11,7 @@ export const loader = async ({ params }: Route.LoaderArgs) => {
const { data: categoriesData } = await getCategories()
const { code } = params
const categoryData = categoriesData.find((category) => category.code === code)
let { data: newsData } = await getNews({ categories: [code] })
newsData = newsData.filter((news) => new Date(news.live_at) <= new Date())
const newsData = getNews({ categories: [code], active: true })
return { categoryData, newsData }
}

View File

@ -1,4 +1,5 @@
import { isRouteErrorResponse } from 'react-router'
import { getClientIPAddress } from 'remix-utils/get-client-ip-address'
import { stripHtml } from 'string-strip-html'
import { getCategories } from '~/apis/common/get-categories'
@ -12,6 +13,8 @@ import { NewsDetailPage } from '~/pages/news-detail'
import type { Route } from './+types/_news.detail.$slug'
export const loader = async ({ request, params }: Route.LoaderArgs) => {
const userAgent = request.headers.get('user-agent')
const ipAddress = getClientIPAddress(request) || 'localhost'
const { userToken: accessToken } = await handleCookie(request)
let userData
if (accessToken) {
@ -19,7 +22,12 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
userData = data
}
const { slug } = params
let { data: newsDetailData } = await getNewsBySlug({ slug, accessToken })
let { data: newsDetailData } = await getNewsBySlug({
slug,
accessToken,
userAgent,
ipAddress,
})
const shouldSubscribe =
(!accessToken || userData?.subscribe?.subscribe_plan?.code === 'basic') &&
newsDetailData?.is_premium
@ -34,13 +42,12 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
const beritaCategory = categoriesData.find(
(category) => category.code === beritaCode,
)
let { data: beritaNews } = await getNews({ categories: [beritaCode] })
beritaNews = beritaNews.filter((news) => new Date(news.live_at) <= new Date())
const beritaData = getNews({ categories: [beritaCode], active: true })
return {
newsDetailData,
beritaCategory,
beritaNews,
beritaData,
shouldSubscribe,
}
}

View File

@ -1,4 +1,5 @@
import { data } from 'react-router'
import { getClientIPAddress } from 'remix-utils/get-client-ip-address'
import { XiorError } from 'xior'
import { createLogAdsRequest } from '~/apis/news/create-log-ads'
@ -7,12 +8,16 @@ import { handleCookie } from '~/libs/cookies'
import type { Route } from './+types/actions.log.ads.$id'
export const action = async ({ request, params }: Route.ActionArgs) => {
const userAgent = request.headers.get('user-agent')
const ipAddress = getClientIPAddress(request) || 'localhost'
const { userToken: accessToken } = await handleCookie(request)
const { id } = params
try {
const { data: logsData } = await createLogAdsRequest({
id,
accessToken,
userAgent,
ipAddress,
})
return data(

View File

@ -1,7 +1,7 @@
import type { TNewsResponse } from '~/apis/common/get-news'
import type { TNewsResponseData } from '~/apis/common/get-news'
export type TNews = {
title: string
description: string
items: TNewsResponse[]
items: Promise<TNewsResponseData>
}

View File

@ -1,4 +1,4 @@
import { decodeJwt } from 'jose'
import { JWT } from '@edgefirst-dev/jwt'
import {
staffTokenCookieConfig,
@ -12,10 +12,10 @@ type TTokenCookie = {
export const generateUserTokenCookie = (parameters: TTokenCookie) => {
const { accessToken } = parameters
const decodedToken = decodeJwt(accessToken)
const decodedToken = JWT.decode(accessToken)
const decodedTokenExp = decodedToken.exp
const expirationDate = decodedTokenExp
? new Date(decodedTokenExp * 1000)
? new Date(Number(decodedTokenExp) * 1000)
: undefined
return userTokenCookieConfig.serialize(accessToken, {
@ -26,10 +26,10 @@ export const generateUserTokenCookie = (parameters: TTokenCookie) => {
export const generateStaffTokenCookie = (parameters: TTokenCookie) => {
const { accessToken } = parameters
const decodedToken = decodeJwt(accessToken)
const decodedToken = JWT.decode(accessToken)
const decodedTokenExp = decodedToken.exp
const expirationDate = decodedTokenExp
? new Date(decodedTokenExp * 1000)
? new Date(Number(decodedTokenExp) * 1000)
: undefined
return staffTokenCookieConfig.serialize(accessToken, {

View File

@ -14,10 +14,15 @@
"validate": "pnpm lint && pnpm typecheck && pnpm knip"
},
"dependencies": {
"@edgefirst-dev/batcher": "^1.0.1",
"@edgefirst-dev/jwt": "^1.2.0",
"@edgefirst-dev/server-timing": "^0.0.1",
"@headlessui/react": "^2.2.0",
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^4.1.1",
"@monaco-editor/react": "^4.7.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@react-router/fs-routes": "^7.1.3",
"@react-router/node": "^7.1.3",
"@react-router/serve": "^7.1.3",
@ -37,8 +42,9 @@
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^8.5.2",
"html-react-parser": "^5.2.2",
"intl-parse-accept-language": "^1.0.0",
"is-ip": "^5.0.1",
"isbot": "^5.1.17",
"jose": "^6.0.8",
"react": "^19.0.0",
"react-chartjs-2": "^5.3.0",
"react-colorful": "^5.6.1",
@ -49,6 +55,7 @@
"react-router": "^7.1.3",
"react-share": "^5.2.2",
"remix-hook-form": "^6.1.3",
"remix-utils": "^8.5.0",
"string-strip-html": "^13.4.12",
"tailwind-merge": "^3.0.1",
"xior": "^0.6.3",

225
pnpm-lock.yaml generated
View File

@ -8,6 +8,15 @@ importers:
.:
dependencies:
'@edgefirst-dev/batcher':
specifier: ^1.0.1
version: 1.0.1
'@edgefirst-dev/jwt':
specifier: ^1.2.0
version: 1.2.0
'@edgefirst-dev/server-timing':
specifier: ^0.0.1
version: 0.0.1
'@headlessui/react':
specifier: ^2.2.0
version: 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -20,6 +29,12 @@ importers:
'@monaco-editor/react':
specifier: ^4.7.0
version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@oslojs/crypto':
specifier: ^1.0.1
version: 1.0.1
'@oslojs/encoding':
specifier: ^1.1.0
version: 1.1.0
'@react-router/fs-routes':
specifier: ^7.1.3
version: 7.1.3(@react-router/dev@7.1.3(@react-router/serve@7.1.3(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3))(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(lightningcss@1.29.1)(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3)(vite@5.4.14(@types/node@20.17.16)(lightningcss@1.29.1)))(typescript@5.7.3)
@ -77,12 +92,15 @@ importers:
html-react-parser:
specifier: ^5.2.2
version: 5.2.2(@types/react@19.0.8)(react@19.0.0)
intl-parse-accept-language:
specifier: ^1.0.0
version: 1.0.0
is-ip:
specifier: ^5.0.1
version: 5.0.1
isbot:
specifier: ^5.1.17
version: 5.1.22
jose:
specifier: ^6.0.8
version: 6.0.8
react:
specifier: ^19.0.0
version: 19.0.0
@ -113,6 +131,9 @@ importers:
remix-hook-form:
specifier: ^6.1.3
version: 6.1.3(react-dom@19.0.0(react@19.0.0))(react-hook-form@7.54.2(react@19.0.0))(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
remix-utils:
specifier: ^8.5.0
version: 8.5.0(@edgefirst-dev/batcher@1.0.1)(@edgefirst-dev/jwt@1.2.0)(@edgefirst-dev/server-timing@0.0.1)(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(intl-parse-accept-language@1.0.0)(is-ip@5.0.1)(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(zod@3.24.2)
string-strip-html:
specifier: ^13.4.12
version: 13.4.12
@ -448,6 +469,22 @@ packages:
resolution: {integrity: sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg==}
engines: {node: '>=v18'}
'@edgefirst-dev/batcher@1.0.1':
resolution: {integrity: sha512-9AsnqLSIbO0mK7Du6lRp3v7hCJuercNo7t16leZeMCnI3QReZDE2IEtbLpDwrIrcOS7xt1vgdonKYU9GQqittw==}
engines: {node: '>=20.0.0'}
'@edgefirst-dev/data@0.0.4':
resolution: {integrity: sha512-VLhlvEPDJ0Sd0pE6sAYTQkIqZCXVonaWlgRJIQQHzfjTXCadF77qqHj5NxaPSc4wCul0DJO/0MnejVqJAXUiRg==}
engines: {node: '>=20.0.0'}
'@edgefirst-dev/jwt@1.2.0':
resolution: {integrity: sha512-MnNceBAmJYhoctIAGYivh0/sSsKYXfEPfwGZ8tsoX96+vSRuoeLrBi4p2L9NHCjqxMafd4KMKk+93SfX3sW7dQ==}
engines: {node: '>=20.0.0'}
'@edgefirst-dev/server-timing@0.0.1':
resolution: {integrity: sha512-WlvF/dhgM7CE9SOb3Ji6Wj4PsIk21CHvXzRVdQwmeS1eVEVimWqagiYuV5e8/7Owt7SDFpeVnuF0q2CtONch0g==}
engines: {node: '>=20.0.0'}
'@emotion/babel-plugin@11.13.5':
resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
@ -747,6 +784,12 @@ packages:
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
'@mjackson/file-storage@0.6.1':
resolution: {integrity: sha512-H3GEVpmfmNryNoYloddIOba5OAwckfVGMvutPeI94Shbv/R+NVh89gIYa8SK3Vfa+ky9PitclP+5XQ6/zlSdQQ==}
'@mjackson/lazy-file@3.3.1':
resolution: {integrity: sha512-BxpNT1KmLx0OLYfgQESx/AKGD2czwfZXh9c0SaDUQY2DRAaVYtAvSQE5EkpATFdQQKqfL+iXVoaQ/SN+w7/CDA==}
'@mjackson/node-fetch-server@0.2.0':
resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==}
@ -803,6 +846,18 @@ packages:
'@one-ini/wasm@0.1.1':
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
'@oslojs/asn1@1.0.0':
resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==}
'@oslojs/binary@1.0.0':
resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==}
'@oslojs/crypto@1.0.1':
resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==}
'@oslojs/encoding@1.1.0':
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@ -2001,6 +2056,10 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
clone-regexp@3.0.0:
resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==}
engines: {node: '>=12'}
clone@1.0.4:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
@ -2081,6 +2140,10 @@ packages:
engines: {node: '>=16'}
hasBin: true
convert-hrtime@5.0.0:
resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==}
engines: {node: '>=12'}
convert-source-map@1.9.0:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
@ -2712,6 +2775,10 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
function-timeout@0.1.1:
resolution: {integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==}
engines: {node: '>=14.16'}
function.prototype.name@1.1.8:
resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==}
engines: {node: '>= 0.4'}
@ -2937,6 +3004,14 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
intl-parse-accept-language@1.0.0:
resolution: {integrity: sha512-YFMSV91JNBOSjw1cOfw2tup6hDP7mkz+2AUV7W1L1AM6ntgI75qC1ZeFpjPGMrWp+upmBRTX2fJWQ8c7jsUWpA==}
engines: {node: '>=14'}
ip-regex@5.0.0:
resolution: {integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@ -3018,6 +3093,10 @@ packages:
resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==}
engines: {node: '>=0.10.0'}
is-ip@5.0.1:
resolution: {integrity: sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==}
engines: {node: '>=14.16'}
is-map@2.0.3:
resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
engines: {node: '>= 0.4'}
@ -3042,6 +3121,10 @@ packages:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'}
is-regexp@3.1.0:
resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==}
engines: {node: '>=12'}
is-set@2.0.3:
resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
engines: {node: '>= 0.4'}
@ -3463,6 +3546,10 @@ packages:
motion-utils@11.18.1:
resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==}
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@ -4096,6 +4183,42 @@ packages:
react-hook-form: ^7.51.0
react-router: '>=7.0.0'
remix-utils@8.5.0:
resolution: {integrity: sha512-Wf9OGSJveBaVHKptbEgxc+DwKRUUGOH+aiaBlsrAA2b4F+gNtCkvaZzA7Tp+1esBElRcRvMZQq/0aSSWFMP18A==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@edgefirst-dev/batcher': ^1.0.0
'@edgefirst-dev/jwt': ^1.2.0
'@edgefirst-dev/server-timing': ^0.0.1
'@oslojs/crypto': ^1.0.1
'@oslojs/encoding': ^1.1.0
intl-parse-accept-language: ^1.0.0
is-ip: ^5.0.1
react: ^18.0.0 || ^19.0.0
react-router: ^7.0.0
zod: ^3.22.4
peerDependenciesMeta:
'@edgefirst-dev/batcher':
optional: true
'@edgefirst-dev/jwt':
optional: true
'@edgefirst-dev/server-timing':
optional: true
'@oslojs/crypto':
optional: true
'@oslojs/encoding':
optional: true
intl-parse-accept-language:
optional: true
is-ip:
optional: true
react:
optional: true
react-router:
optional: true
zod:
optional: true
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@ -4403,6 +4526,10 @@ packages:
summary@2.1.0:
resolution: {integrity: sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==}
super-regex@0.2.0:
resolution: {integrity: sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==}
engines: {node: '>=14.16'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -4437,6 +4564,10 @@ packages:
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
time-span@5.1.0:
resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==}
engines: {node: '>=12'}
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@ -4507,6 +4638,10 @@ packages:
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
engines: {node: '>=8'}
type-fest@4.37.0:
resolution: {integrity: sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==}
engines: {node: '>=16'}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@ -5105,6 +5240,21 @@ snapshots:
'@types/conventional-commits-parser': 5.0.1
chalk: 5.4.1
'@edgefirst-dev/batcher@1.0.1':
dependencies:
type-fest: 4.37.0
'@edgefirst-dev/data@0.0.4': {}
'@edgefirst-dev/jwt@1.2.0':
dependencies:
'@edgefirst-dev/data': 0.0.4
'@mjackson/file-storage': 0.6.1
jose: 6.0.8
type-fest: 4.37.0
'@edgefirst-dev/server-timing@0.0.1': {}
'@emotion/babel-plugin@11.13.5':
dependencies:
'@babel/helper-module-imports': 7.25.9
@ -5391,6 +5541,14 @@ snapshots:
'@kurkle/color@0.3.4': {}
'@mjackson/file-storage@0.6.1':
dependencies:
'@mjackson/lazy-file': 3.3.1
'@mjackson/lazy-file@3.3.1':
dependencies:
mrmime: 2.0.1
'@mjackson/node-fetch-server@0.2.0': {}
'@monaco-editor/loader@1.5.0':
@ -5461,6 +5619,19 @@ snapshots:
'@one-ini/wasm@0.1.1': {}
'@oslojs/asn1@1.0.0':
dependencies:
'@oslojs/binary': 1.0.0
'@oslojs/binary@1.0.0': {}
'@oslojs/crypto@1.0.1':
dependencies:
'@oslojs/asn1': 1.0.0
'@oslojs/binary': 1.0.0
'@oslojs/encoding@1.1.0': {}
'@pkgjs/parseargs@0.11.0':
optional: true
@ -6698,6 +6869,10 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
clone-regexp@3.0.0:
dependencies:
is-regexp: 3.1.0
clone@1.0.4:
optional: true
@ -6779,6 +6954,8 @@ snapshots:
meow: 12.1.1
split2: 4.2.0
convert-hrtime@5.0.0: {}
convert-source-map@1.9.0: {}
convert-source-map@2.0.0: {}
@ -7576,6 +7753,8 @@ snapshots:
function-bind@1.1.2: {}
function-timeout@0.1.1: {}
function.prototype.name@1.1.8:
dependencies:
call-bind: 1.0.8
@ -7805,6 +7984,10 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
intl-parse-accept-language@1.0.0: {}
ip-regex@5.0.0: {}
ipaddr.js@1.9.1: {}
is-array-buffer@3.0.5:
@ -7886,6 +8069,11 @@ snapshots:
is-gzip@1.0.0: {}
is-ip@5.0.1:
dependencies:
ip-regex: 5.0.0
super-regex: 0.2.0
is-map@2.0.3: {}
is-number-object@1.1.1:
@ -7906,6 +8094,8 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.2
is-regexp@3.1.0: {}
is-set@2.0.3: {}
is-shared-array-buffer@1.0.4:
@ -8291,6 +8481,8 @@ snapshots:
motion-utils@11.18.1: {}
mrmime@2.0.1: {}
ms@2.0.0: {}
ms@2.1.3: {}
@ -8936,6 +9128,21 @@ snapshots:
react-hook-form: 7.54.2(react@19.0.0)
react-router: 7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
remix-utils@8.5.0(@edgefirst-dev/batcher@1.0.1)(@edgefirst-dev/jwt@1.2.0)(@edgefirst-dev/server-timing@0.0.1)(@oslojs/crypto@1.0.1)(@oslojs/encoding@1.1.0)(intl-parse-accept-language@1.0.0)(is-ip@5.0.1)(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(zod@3.24.2):
dependencies:
type-fest: 4.37.0
optionalDependencies:
'@edgefirst-dev/batcher': 1.0.1
'@edgefirst-dev/jwt': 1.2.0
'@edgefirst-dev/server-timing': 0.0.1
'@oslojs/crypto': 1.0.1
'@oslojs/encoding': 1.1.0
intl-parse-accept-language: 1.0.0
is-ip: 5.0.1
react: 19.0.0
react-router: 7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
zod: 3.24.2
require-directory@2.1.1: {}
require-from-string@2.0.2: {}
@ -9300,6 +9507,12 @@ snapshots:
summary@2.1.0: {}
super-regex@0.2.0:
dependencies:
clone-regexp: 3.0.0
function-timeout: 0.1.1
time-span: 5.1.0
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@ -9325,6 +9538,10 @@ snapshots:
through@2.3.8: {}
time-span@5.1.0:
dependencies:
convert-hrtime: 5.0.0
tiny-invariant@1.3.3: {}
tiny-lru@11.2.11: {}
@ -9377,6 +9594,8 @@ snapshots:
type-fest@0.8.1: {}
type-fest@4.37.0: {}
type-is@1.6.18:
dependencies:
media-typer: 0.3.0