Compare commits
19 Commits
ecd1900acc
...
e7eda086e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7eda086e4 | ||
|
|
550b16cfd3 | ||
|
|
0b3217407c | ||
|
|
7afabdaa03 | ||
|
|
9f82779456 | ||
|
|
ee816b8db7 | ||
|
|
1b6321f2bd | ||
|
|
4847ef2be3 | ||
|
|
bad5294030 | ||
|
|
9976626f97 | ||
|
|
d38bf3a705 | ||
|
|
687e3c8d01 | ||
|
|
4554cca392 | ||
|
|
05b3af8718 | ||
|
|
e2e3095c33 | ||
|
|
5b94c6df91 | ||
|
|
634c809489 | ||
|
|
0da2006e78 | ||
|
|
09e5f84b8b |
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { HttpServer } from '~/libs/http-server'
|
||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||
import type { TCategorySchema } from '~/pages/form-category'
|
||||
|
||||
const categoryResponseSchema = z.object({
|
||||
@ -10,14 +10,13 @@ const categoryResponseSchema = z.object({
|
||||
})
|
||||
|
||||
type TParameters = {
|
||||
accessToken: string
|
||||
payload: TCategorySchema
|
||||
}
|
||||
} & THttpServer
|
||||
|
||||
export const createCategoryRequest = async (parameters: TParameters) => {
|
||||
const { accessToken, payload } = parameters
|
||||
const { payload, ...restParameters } = parameters
|
||||
try {
|
||||
const { data } = await HttpServer({ accessToken }).post(
|
||||
const { data } = await HttpServer(restParameters).post(
|
||||
'/api/category/create',
|
||||
payload,
|
||||
)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { HttpServer } from '~/libs/http-server'
|
||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||
import type { TContentSchema } from '~/pages/form-contents'
|
||||
|
||||
const newsResponseSchema = z.object({
|
||||
@ -10,21 +10,20 @@ const newsResponseSchema = z.object({
|
||||
})
|
||||
|
||||
type TParameter = {
|
||||
accessToken: string
|
||||
payload: TContentSchema
|
||||
}
|
||||
} & THttpServer
|
||||
|
||||
export const createNewsRequest = async (parameters: TParameter) => {
|
||||
const { accessToken, payload } = parameters
|
||||
const { payload, ...restParameters } = parameters
|
||||
const { categories, tags, live_at, ...restPayload } = payload
|
||||
const transformedPayload = {
|
||||
...restPayload,
|
||||
categories: categories.map((category) => category?.id),
|
||||
tags: tags?.map((tag) => tag?.id) || [],
|
||||
live_at: new Date(live_at).toISOString(),
|
||||
}
|
||||
try {
|
||||
const { categories, tags, ...restPayload } = payload
|
||||
const transformedPayload = {
|
||||
...restPayload,
|
||||
categories: categories.map((category) => category?.id),
|
||||
tags: tags?.map((tag) => tag?.id) || [],
|
||||
live_at: new Date(payload?.live_at).toISOString(),
|
||||
}
|
||||
const { data } = await HttpServer({ accessToken }).post(
|
||||
const { data } = await HttpServer(restParameters).post(
|
||||
'/api/news/create',
|
||||
transformedPayload,
|
||||
)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { HttpServer } from '~/libs/http-server'
|
||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||
import type { TTagSchema } from '~/pages/form-tag'
|
||||
|
||||
const tagsResponseSchema = z.object({
|
||||
@ -10,21 +10,15 @@ const tagsResponseSchema = z.object({
|
||||
})
|
||||
|
||||
type TParameters = {
|
||||
accessToken: string
|
||||
payload: TTagSchema
|
||||
}
|
||||
} & THttpServer
|
||||
|
||||
export const createTagsRequest = async (parameters: TParameters) => {
|
||||
const { accessToken, payload } = parameters
|
||||
const { payload, ...restParameters } = parameters
|
||||
try {
|
||||
const { ...restPayload } = payload
|
||||
const transformedPayload = {
|
||||
...restPayload,
|
||||
}
|
||||
|
||||
const { data } = await HttpServer({ accessToken }).post(
|
||||
const { data } = await HttpServer(restParameters).post(
|
||||
'/api/tag/create',
|
||||
transformedPayload,
|
||||
payload,
|
||||
)
|
||||
return tagsResponseSchema.parse(data)
|
||||
} catch (error) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { HttpServer } from '~/libs/http-server'
|
||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||
import type { TCategorySchema } from '~/pages/form-category'
|
||||
|
||||
const categoryResponseSchema = z.object({
|
||||
@ -10,15 +10,14 @@ const categoryResponseSchema = z.object({
|
||||
})
|
||||
|
||||
type TParameters = {
|
||||
accessToken: string
|
||||
payload: TCategorySchema
|
||||
}
|
||||
} & THttpServer
|
||||
|
||||
export const updateCategoryRequest = async (parameters: TParameters) => {
|
||||
const { accessToken, payload } = parameters
|
||||
const { payload, ...restParameters } = parameters
|
||||
const { id, ...restPayload } = payload
|
||||
try {
|
||||
const { id, ...restPayload } = payload
|
||||
const { data } = await HttpServer({ accessToken }).put(
|
||||
const { data } = await HttpServer(restParameters).put(
|
||||
`/api/category/${id}/update`,
|
||||
restPayload,
|
||||
)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { HttpServer } from '~/libs/http-server'
|
||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||
import type { TContentSchema } from '~/pages/form-contents'
|
||||
|
||||
const newsResponseSchema = z.object({
|
||||
@ -10,21 +10,20 @@ const newsResponseSchema = z.object({
|
||||
})
|
||||
|
||||
type TParameter = {
|
||||
accessToken: string
|
||||
payload: TContentSchema
|
||||
}
|
||||
} & THttpServer
|
||||
|
||||
export const updateNewsRequest = async (parameters: TParameter) => {
|
||||
const { accessToken, payload } = parameters
|
||||
const { payload, ...restParameters } = parameters
|
||||
const { categories, tags, id, live_at, ...restPayload } = payload
|
||||
const transformedPayload = {
|
||||
...restPayload,
|
||||
categories: categories.map((category) => category?.id),
|
||||
tags: tags?.map((tag) => tag?.id) || [],
|
||||
live_at: new Date(live_at).toISOString(),
|
||||
}
|
||||
try {
|
||||
const { categories, tags, id, ...restPayload } = payload
|
||||
const transformedPayload = {
|
||||
...restPayload,
|
||||
categories: categories.map((category) => category?.id),
|
||||
tags: tags?.map((tag) => tag?.id) || [],
|
||||
live_at: new Date(payload?.live_at).toISOString(),
|
||||
}
|
||||
const { data } = await HttpServer({ accessToken }).put(
|
||||
const { data } = await HttpServer(restParameters).put(
|
||||
`/api/news/${id}/update`,
|
||||
transformedPayload,
|
||||
)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { HttpServer } from '~/libs/http-server'
|
||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||
import type { TTagSchema } from '~/pages/form-tag'
|
||||
|
||||
const tagResponseSchema = z.object({
|
||||
@ -10,15 +10,14 @@ const tagResponseSchema = z.object({
|
||||
})
|
||||
|
||||
type TParameters = {
|
||||
accessToken: string
|
||||
payload: TTagSchema
|
||||
}
|
||||
} & THttpServer
|
||||
|
||||
export const updateTagRequest = async (parameters: TParameters) => {
|
||||
const { accessToken, payload } = parameters
|
||||
const { payload, ...restParameters } = parameters
|
||||
const { id, ...restPayload } = payload
|
||||
try {
|
||||
const { id, ...restPayload } = payload
|
||||
const { data } = await HttpServer({ accessToken }).put(
|
||||
const { data } = await HttpServer(restParameters).put(
|
||||
`/api/tag/${id}/update`,
|
||||
restPayload,
|
||||
)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { newsResponseSchema } from '~/apis/admin/get-news'
|
||||
import { newsResponseSchema } from '~/apis/common/get-news'
|
||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||
|
||||
const dataResponseSchema = z.object({
|
||||
@ -12,9 +12,9 @@ type TParameters = {
|
||||
} & THttpServer
|
||||
|
||||
export const getNewsBySlug = async (parameters: TParameters) => {
|
||||
const { slug, accessToken } = parameters
|
||||
const { slug, ...restParameters } = parameters
|
||||
try {
|
||||
const { data } = await HttpServer({ accessToken }).get(`/api/news/${slug}`)
|
||||
const { data } = await HttpServer(restParameters).get(`/api/news/${slug}`)
|
||||
return dataResponseSchema.parse(data)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
||||
|
||||
@ -30,10 +30,20 @@ const dataResponseSchema = z.object({
|
||||
|
||||
export type TNewsResponse = z.infer<typeof newsResponseSchema>
|
||||
export type TAuthor = z.infer<typeof authorSchema>
|
||||
type TParameters = {
|
||||
categories?: string[]
|
||||
tags?: string[]
|
||||
} & THttpServer
|
||||
|
||||
export const getNews = async (parameters: THttpServer) => {
|
||||
export const getNews = async (parameters?: TParameters) => {
|
||||
const { categories, tags, ...restParameters } = parameters || {}
|
||||
try {
|
||||
const { data } = await HttpServer(parameters).get(`/api/news`)
|
||||
const { data } = await HttpServer(restParameters).get(`/api/news`, {
|
||||
params: {
|
||||
...(categories && { categories: categories.join('+') }),
|
||||
...(tags && { tags: tags.join('+') }),
|
||||
},
|
||||
})
|
||||
return dataResponseSchema.parse(data)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
||||
@ -11,7 +11,7 @@ const userResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
subscribe_plan_id: z.string(),
|
||||
start_date: z.string(),
|
||||
end_date: z.string(),
|
||||
end_date: z.string().nullable(),
|
||||
status: z.string(),
|
||||
auto_renew: z.boolean(),
|
||||
subscribe_plan: z.object({
|
||||
|
||||
@ -4,12 +4,12 @@ import { HttpServer } from '~/libs/http-server'
|
||||
import { loginResponseSchema } from './login-user'
|
||||
|
||||
export const userRegisterRequest = async (payload: TRegisterSchema) => {
|
||||
const { subscribe_plan, ...restPayload } = payload
|
||||
const transformedPayload = {
|
||||
...restPayload,
|
||||
subscribe_plan_id: subscribe_plan.id,
|
||||
}
|
||||
try {
|
||||
const { subscribe_plan, ...restPayload } = payload
|
||||
const transformedPayload = {
|
||||
...restPayload,
|
||||
subscribe_plan_id: subscribe_plan.id,
|
||||
}
|
||||
const { data } = await HttpServer().post(
|
||||
'/api/user/register',
|
||||
transformedPayload,
|
||||
|
||||
@ -23,6 +23,10 @@ body {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.ProseMirror-trailingBreak {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
table.dataTable thead > tr {
|
||||
border-bottom: 2px solid #c2c2c2;
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ const DESCRIPTIONS: DescriptionMap = {
|
||||
export const SuccessModal = ({ isOpen, onClose }: ModalProperties) => {
|
||||
const { setIsLoginOpen, setIsSubscribeOpen } = useNewsContext()
|
||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
||||
const userData = loaderData?.userData
|
||||
const { userData } = loaderData || {}
|
||||
|
||||
const message = isOpen
|
||||
? DESCRIPTIONS[isOpen]
|
||||
|
||||
@ -67,7 +67,13 @@ export const TextEditor = <TFormValues extends Record<string, unknown>>(
|
||||
const editor = useEditor({
|
||||
editable: !disabled,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
StarterKit.configure({
|
||||
paragraph: {
|
||||
HTMLAttributes: {
|
||||
class: 'min-h-1',
|
||||
},
|
||||
},
|
||||
}),
|
||||
Highlight,
|
||||
Image.configure({
|
||||
inline: true,
|
||||
@ -134,7 +140,7 @@ export const TextEditor = <TFormValues extends Record<string, unknown>>(
|
||||
editor={editor}
|
||||
id={id ?? generatedId}
|
||||
className={twMerge(
|
||||
'prose prose-headings:my-0 prose-p:my-0 max-h-96 max-w-none cursor-text overflow-y-auto rounded-b-md p-2',
|
||||
'prose prose-headings:my-0.5 prose-p:my-0.5 max-h-96 max-w-none cursor-text overflow-y-auto rounded-b-md p-2',
|
||||
inputClassName,
|
||||
)}
|
||||
onClick={() => editor?.commands.focus()}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import Autoplay from 'embla-carousel-autoplay'
|
||||
import useEmblaCarousel from 'embla-carousel-react'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import { BANNER } from '~/data/contents'
|
||||
|
||||
export const Banner = () => {
|
||||
const [emblaReference] = useEmblaCarousel({ loop: false })
|
||||
const [emblaReference] = useEmblaCarousel({ loop: true }, [Autoplay()])
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
|
||||
@ -3,11 +3,11 @@ import type { ComponentProps, FC, PropsWithChildren } from 'react'
|
||||
type TProperties = PropsWithChildren<ComponentProps<'div'>>
|
||||
|
||||
export const Card: FC<TProperties> = (properties) => {
|
||||
const { children, ...rest } = properties
|
||||
const { children, ...restProperties } = properties
|
||||
return (
|
||||
<div
|
||||
className="border-[.2px] border-black/20 bg-white p-[30px]"
|
||||
{...rest}
|
||||
{...restProperties}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import useEmblaCarousel from 'embla-carousel-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useRouteLoaderData } from 'react-router'
|
||||
import { stripHtml } from 'string-strip-html'
|
||||
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { CarouselButton } from '~/components/ui/button-slide'
|
||||
@ -12,7 +13,7 @@ import { getPremiumAttribute } from '~/utils/render'
|
||||
export const CarouselHero = (properties: TNews) => {
|
||||
const { setIsSuccessOpen } = useNewsContext()
|
||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
||||
const userData = loaderData?.userData
|
||||
const { userData } = loaderData || {}
|
||||
const { title, description, items } = properties
|
||||
const [emblaReference, emblaApi] = useEmblaCarousel({ loop: false })
|
||||
|
||||
@ -71,41 +72,43 @@ export const CarouselHero = (properties: TNews) => {
|
||||
ref={emblaReference}
|
||||
>
|
||||
<div className="embla__container hero flex sm:gap-x-8">
|
||||
{items.map(({ featured, title, content, slug, isPremium }, 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}
|
||||
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 text-[#777777] sm:text-xl">
|
||||
{content}
|
||||
</p>
|
||||
{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,
|
||||
})}
|
||||
>
|
||||
View More
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
size="block"
|
||||
{...getPremiumAttribute({
|
||||
isPremium,
|
||||
slug,
|
||||
onClick: () => setIsSuccessOpen('warning'),
|
||||
userData,
|
||||
})}
|
||||
>
|
||||
View More
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import useEmblaCarousel from 'embla-carousel-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useRouteLoaderData } from 'react-router'
|
||||
import { stripHtml } from 'string-strip-html'
|
||||
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { CarouselButton } from '~/components/ui/button-slide'
|
||||
@ -9,10 +10,12 @@ import type { loader } from '~/routes/_news'
|
||||
import type { TNews } from '~/types/news'
|
||||
import { getPremiumAttribute } from '~/utils/render'
|
||||
|
||||
import { Tags } from './tags'
|
||||
|
||||
export const CarouselSection = (properties: TNews) => {
|
||||
const { setIsSuccessOpen } = useNewsContext()
|
||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
||||
const userData = loaderData?.userData
|
||||
const { userData } = loaderData || {}
|
||||
const { title, description, items } = properties
|
||||
const [emblaReference, emblaApi] = useEmblaCarousel({
|
||||
loop: false,
|
||||
@ -77,46 +80,38 @@ export const CarouselSection = (properties: TNews) => {
|
||||
>
|
||||
<div className="embla__container col-span-3 flex max-h-[586px] sm:gap-x-8">
|
||||
{items.map(
|
||||
({ featured, title, content, tags, slug, isPremium }, index) => (
|
||||
(
|
||||
{ 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">
|
||||
<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}
|
||||
src={featured_image}
|
||||
alt={title}
|
||||
/>
|
||||
<div className={'flex flex-col justify-between gap-4'}>
|
||||
<div className={'flex text-sm uppercase'}>
|
||||
{tags?.map((item) => (
|
||||
<span
|
||||
key={index}
|
||||
className="my-3 mr-2 inline-block rounded bg-[#F4F4F4] px-3 py-1 font-bold text-[#777777]"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
{isPremium && (
|
||||
<span className="my-3 mr-2 inline-block rounded bg-[#D1C675] px-3 py-1 font-bold text-[#9D761D]">
|
||||
Premium Content
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Tags
|
||||
tags={tags || []}
|
||||
is_premium={is_premium}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h3 className="mt-2 w-full text-xl font-bold sm:text-2xl lg:mt-0">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-md mt-5 text-[#777777] sm:text-xl">
|
||||
{content}
|
||||
<p className="text-md mt-5 line-clamp-3 text-[#777777] sm:text-xl">
|
||||
{stripHtml(content).result}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="block"
|
||||
{...getPremiumAttribute({
|
||||
isPremium,
|
||||
isPremium: is_premium,
|
||||
slug,
|
||||
onClick: () => setIsSuccessOpen('warning'),
|
||||
userData,
|
||||
|
||||
@ -1,18 +1,27 @@
|
||||
import { useRouteLoaderData } from 'react-router'
|
||||
import { stripHtml } from 'string-strip-html'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import type { TNewsResponse } from '~/apis/common/get-news'
|
||||
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')
|
||||
const userData = loaderData?.userData
|
||||
const { userData } = loaderData || {}
|
||||
|
||||
const { title, description, items } = properties
|
||||
|
||||
@ -39,38 +48,26 @@ export const CategorySection = (properties: TNews) => {
|
||||
|
||||
<div className="grid sm:grid-cols-3 sm:gap-x-8">
|
||||
{items.map(
|
||||
({ featured, title, content, tags, slug, isPremium }, index) => (
|
||||
(
|
||||
{ featured_image, title, content, tags, slug, is_premium },
|
||||
index,
|
||||
) => (
|
||||
<div
|
||||
key={index}
|
||||
className={twMerge('grid sm:gap-x-8')}
|
||||
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}
|
||||
src={featured_image}
|
||||
alt={title}
|
||||
/>
|
||||
<div className={twMerge('flex flex-col justify-between gap-4')}>
|
||||
<div
|
||||
className={twMerge(
|
||||
'my-3 flex gap-2 uppercase max-sm:text-sm',
|
||||
)}
|
||||
>
|
||||
{tags?.map((item) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block rounded bg-[#F4F4F4] px-3 py-1 font-bold text-[#777777]"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
{isPremium && (
|
||||
<span className="inline-block rounded bg-[#D1C675] px-3 py-1 font-bold text-[#9D761D]">
|
||||
Premium Content
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Tags
|
||||
tags={tags}
|
||||
is_premium={is_premium}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h3
|
||||
@ -80,14 +77,14 @@ export const CategorySection = (properties: TNews) => {
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-md mt-5 text-[#777777] sm:text-xl">
|
||||
{content}
|
||||
<p className="text-md mt-5 line-clamp-3 text-[#777777] sm:text-xl">
|
||||
{stripHtml(content).result}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="block"
|
||||
{...getPremiumAttribute({
|
||||
isPremium,
|
||||
isPremium: is_premium,
|
||||
slug,
|
||||
onClick: () => setIsSuccessOpen('warning'),
|
||||
userData,
|
||||
|
||||
@ -53,7 +53,7 @@ export const Combobox = <TFormValues extends Record<string, unknown>>(
|
||||
className,
|
||||
labelClassName,
|
||||
containerClassName,
|
||||
...rest
|
||||
...restProperties
|
||||
} = properties
|
||||
const {
|
||||
control,
|
||||
@ -88,7 +88,7 @@ export const Combobox = <TFormValues extends Record<string, unknown>>(
|
||||
onChange={field.onChange}
|
||||
disabled={disabled}
|
||||
immediate
|
||||
{...rest}
|
||||
{...restProperties}
|
||||
>
|
||||
<div className="relative">
|
||||
<ComboboxInput
|
||||
|
||||
@ -40,7 +40,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
|
||||
className,
|
||||
containerClassName,
|
||||
labelClassName,
|
||||
...rest
|
||||
...restProperties
|
||||
} = properties
|
||||
const [inputType, setInputType] = useState(type)
|
||||
|
||||
@ -68,7 +68,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
|
||||
)}
|
||||
placeholder={inputType === 'password' ? '******' : placeholder}
|
||||
{...register(name, rules)}
|
||||
{...rest}
|
||||
{...restProperties}
|
||||
/>
|
||||
{type === 'password' && (
|
||||
<Button
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { TAuthor } from '~/apis/admin/get-news'
|
||||
import type { TAuthor } from '~/apis/common/get-news'
|
||||
import { ProfileIcon } from '~/components/icons/profile'
|
||||
import { formatDate } from '~/utils/formatter'
|
||||
|
||||
|
||||
@ -35,37 +35,44 @@ export const SocialShareButtons = ({
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
{showPopup && (
|
||||
<div className="absolute top-0 rounded-lg border-2 border-gray-400 bg-white p-2 shadow-lg">
|
||||
Link berhasil disalin!
|
||||
</div>
|
||||
)}
|
||||
<button onClick={handleCopyLink}>
|
||||
<LinkIcon className="h-8 w-8 rounded-full bg-gray-400 p-2 sm:h-10 sm:w-10" />
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="relative cursor-pointer"
|
||||
>
|
||||
<LinkIcon className="h-8 w-8 rounded-full bg-[#F4F4F4] p-2 sm:h-10 sm:w-10" />
|
||||
{showPopup && (
|
||||
<div className="absolute top-12 w-48 rounded-lg border-2 border-gray-400 bg-white p-2 shadow-lg">
|
||||
Link berhasil disalin!
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<FacebookShareButton
|
||||
url={url}
|
||||
title={title}
|
||||
>
|
||||
<FacebookIcon className="h-8 w-8 rounded-full bg-gray-400 p-2 sm:h-10 sm:w-10" />
|
||||
<FacebookIcon className="h-8 w-8 rounded-full bg-[#F4F4F4] p-2 sm:h-10 sm:w-10" />
|
||||
</FacebookShareButton>
|
||||
|
||||
<LinkedinShareButton
|
||||
url={url}
|
||||
title={title}
|
||||
>
|
||||
<LinkedinIcon className="h-8 w-8 rounded-full bg-gray-400 p-2 sm:h-10 sm:w-10" />
|
||||
<LinkedinIcon className="h-8 w-8 rounded-full bg-[#F4F4F4] p-2 sm:h-10 sm:w-10" />
|
||||
</LinkedinShareButton>
|
||||
|
||||
<TwitterShareButton
|
||||
url={url}
|
||||
title={title}
|
||||
>
|
||||
<XIcon className="h-8 w-8 rounded-full bg-gray-400 p-2 sm:h-10 sm:w-10" />
|
||||
<XIcon className="h-8 w-8 rounded-full bg-[#F4F4F4] p-2 sm:h-10 sm:w-10" />
|
||||
</TwitterShareButton>
|
||||
|
||||
<button onClick={handleInstagramShare}>
|
||||
<InstagramIcon className="h-8 w-8 rounded-full bg-gray-400 p-2 sm:h-10 sm:w-10" />
|
||||
<button
|
||||
onClick={handleInstagramShare}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<InstagramIcon className="h-8 w-8 rounded-full bg-[#F4F4F4] p-2 sm:h-10 sm:w-10" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
30
app/components/ui/tags.tsx
Normal file
30
app/components/ui/tags.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import type { TTagResponse } from '~/apis/common/get-tags'
|
||||
|
||||
type TProperties = {
|
||||
tags: TTagResponse[]
|
||||
is_premium?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const Tags = (properties: TProperties) => {
|
||||
const { tags, is_premium, className } = properties
|
||||
return (
|
||||
<div className={twMerge('flex gap-2 uppercase max-sm:text-sm', className)}>
|
||||
{tags?.map((tag) => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="inline-block rounded bg-[#F4F4F4] px-3 py-1 font-bold text-[#777777]"
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
))}
|
||||
{is_premium && (
|
||||
<span className="inline-block rounded bg-[#D1C675] px-3 py-1 font-bold text-[#9D761D]">
|
||||
Premium Content
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
import type { TNews } from '~/types/news'
|
||||
type TBanner = {
|
||||
id: number
|
||||
urlImage: string
|
||||
@ -8,93 +7,6 @@ type TBanner = {
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export const DUMMY_DESCRIPTION = 'Berita Terhangat hari ini'
|
||||
|
||||
export const SPOTLIGHT: TNews = {
|
||||
title: 'SPOTLIGHT',
|
||||
description: DUMMY_DESCRIPTION,
|
||||
items: [
|
||||
{
|
||||
title: '01 Hotman Paris Membuka Perpustakaan di tengah Diskotik',
|
||||
content:
|
||||
'Pengacara Kondang, Hotman Paris Hutapea, membuka sebuah perpustakaan baru di dalam diskotik nya yang berlokasi di daerah Jakarta Pusat, Hotman berkata Perpustakaan ini dibuka dengan harapan untuk meningkatkan gairah membaca masyarakat Indonesia, namun sayangnya..',
|
||||
featured: '/images/news-1.jpg',
|
||||
slug: 'hotman-paris-membuka-perpustakaan-di-tengah-diskotik',
|
||||
},
|
||||
{
|
||||
title: '02 Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Pengacara Kondang, Hotman Paris Hutapea, membuka sebuah perpustakaan baru di dalam diskotik nya yang berlokasi di daerah Jakarta Pusat, Hotman berkata Perpustakaan ini dibuka dengan harapan untuk meningkatkan gairah membaca masyarakat Indonesia, namun sayangnya..',
|
||||
featured: 'https://placehold.co/600x400.png',
|
||||
slug: 'hotman-paris-membuka-perpustakaan-di-tengah-diskotik',
|
||||
},
|
||||
{
|
||||
title: '03 Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Pengacara Kondang, Hotman Paris Hutapea, membuka sebuah perpustakaan baru di dalam diskotik nya yang berlokasi di daerah Jakarta Pusat, Hotman berkata Perpustakaan ini dibuka dengan harapan untuk meningkatkan gairah membaca masyarakat Indonesia, namun sayangnya..',
|
||||
featured: '/images/news-1.jpg',
|
||||
slug: 'hotman-paris-membuka-perpustakaan-di-tengah-diskotik',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const BERITA: TNews = {
|
||||
title: 'BERITA',
|
||||
description: DUMMY_DESCRIPTION,
|
||||
items: [
|
||||
{
|
||||
title: '01 Travelling as a way of self-discovery and progress ',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: '02 How does writing influence your personal brand?',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-3.jpg',
|
||||
tags: ['Hukum'],
|
||||
slug: 'how-does-writing-influence-your-personal-brand',
|
||||
},
|
||||
{
|
||||
title: '03 Helping a local business reinvent itself',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-4.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
isPremium: true,
|
||||
slug: 'helping-a-local-business-reinvent-itself',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: 'https://placehold.co/600x400.png',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'How does writing influence your personal brand?',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-3.jpg',
|
||||
tags: ['Hukum'],
|
||||
slug: 'how-does-writing-influence-your-personal-brand',
|
||||
},
|
||||
{
|
||||
title: 'Helping a local business reinvent itself',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-4.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
isPremium: true,
|
||||
slug: 'helping-a-local-business-reinvent-itself',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const BANNER: TBanner[] = [
|
||||
{
|
||||
id: 1,
|
||||
|
||||
@ -8,7 +8,7 @@ import type { loader } from '~/routes/_admin.lg-admin'
|
||||
|
||||
export const Navbar = () => {
|
||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_admin.lg-admin')
|
||||
const staffData = loaderData?.staffData
|
||||
const { staffData } = loaderData || {}
|
||||
const fetcher = useFetcher()
|
||||
|
||||
return (
|
||||
@ -25,7 +25,7 @@ export const Navbar = () => {
|
||||
</Link>
|
||||
<div className="flex items-center gap-x-8">
|
||||
<Popover className="relative">
|
||||
<PopoverButton className="flex w-3xs cursor-pointer items-center justify-between rounded-xl p-2 ring-2 ring-[#707FDD]/10 hover:shadow focus:outline-none">
|
||||
<PopoverButton className="flex w-3xs cursor-pointer items-center justify-between rounded-xl p-2 ring-1 ring-[#707FDD]/10 hover:shadow focus:outline-none">
|
||||
<div className="flex items-center space-x-3">
|
||||
{staffData?.profile_picture ? (
|
||||
<img
|
||||
@ -37,7 +37,7 @@ export const Navbar = () => {
|
||||
<ProfileIcon className="h-8 w-8 rounded-full bg-[#C4C4C4]" />
|
||||
)}
|
||||
|
||||
<span className="text-xs">{staffData?.name}</span>
|
||||
<span className="text-sm">{staffData?.name}</span>
|
||||
</div>
|
||||
<ChevronIcon className="opacity-50" />
|
||||
</PopoverButton>
|
||||
|
||||
@ -43,7 +43,7 @@ export const FormRegister = () => {
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const fetcher = useFetcher()
|
||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
||||
const subscriptions = loaderData?.subscriptionsData
|
||||
const { subscriptionsData: subscriptions } = loaderData || {}
|
||||
|
||||
const formMethods = useRemixForm<TRegisterSchema>({
|
||||
mode: 'onSubmit',
|
||||
|
||||
@ -31,7 +31,7 @@ export default function FormSubscription() {
|
||||
const [error, setError] = useState<string>()
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
||||
const subscriptions = loaderData?.subscriptionsData
|
||||
const { subscriptionsData: subscriptions } = loaderData || {}
|
||||
|
||||
const formMethods = useRemixForm<TSubscribeSchema>({
|
||||
mode: 'onSubmit',
|
||||
|
||||
@ -18,7 +18,7 @@ export default function HeaderMenuMobile(properties: THeaderMenuMobile) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const { setIsLoginOpen } = useNewsContext()
|
||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
||||
const userData = loaderData?.userData
|
||||
const { userData } = loaderData || {}
|
||||
const fetcher = useFetcher()
|
||||
|
||||
const handleToggleMenu = (): void => {
|
||||
|
||||
@ -8,7 +8,7 @@ import type { loader } from '~/routes/_news'
|
||||
export const HeaderTop = () => {
|
||||
const { setIsLoginOpen } = useNewsContext()
|
||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
||||
const userData = loaderData?.userData
|
||||
const { userData } = loaderData || {}
|
||||
const fetcher = useFetcher()
|
||||
|
||||
return (
|
||||
|
||||
@ -6,15 +6,14 @@ import type { TCategoryResponse } from '~/apis/common/get-categories'
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { UiTable } from '~/components/ui/table'
|
||||
import { TitleDashboard } from '~/components/ui/title-dashboard'
|
||||
import type { loader } from '~/routes/_admin.lg-admin._dashboard.categories._index'
|
||||
import type { loader } from '~/routes/_admin.lg-admin._dashboard'
|
||||
export const CategoriesPage = () => {
|
||||
const loaderData = useRouteLoaderData<typeof loader>(
|
||||
'routes/_admin.lg-admin._dashboard.categories._index',
|
||||
'routes/_admin.lg-admin._dashboard',
|
||||
)
|
||||
const categoriesData = loaderData?.dataCategories
|
||||
|
||||
DataTable.use(DT)
|
||||
const dataTable = categoriesData?.sort((a, b) => {
|
||||
const dataTable = loaderData?.categoriesData?.sort((a, b) => {
|
||||
if (a.sequence === null) return 1
|
||||
if (b.sequence === null) return -1
|
||||
return a.sequence - b.sequence
|
||||
|
||||
@ -2,8 +2,8 @@ import DT from 'datatables.net-dt'
|
||||
import DataTable from 'datatables.net-react'
|
||||
import { Link, useRouteLoaderData } from 'react-router'
|
||||
|
||||
import type { TNewsResponse } from '~/apis/admin/get-news'
|
||||
import type { TCategoryResponse } from '~/apis/common/get-categories'
|
||||
import type { TNewsResponse } from '~/apis/common/get-news'
|
||||
import type { TTagResponse } from '~/apis/common/get-tags'
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { UiTable } from '~/components/ui/table'
|
||||
@ -15,10 +15,9 @@ export const ContentsPage = () => {
|
||||
const loaderData = useRouteLoaderData<typeof loader>(
|
||||
'routes/_admin.lg-admin._dashboard.contents._index',
|
||||
)
|
||||
const newsData = loaderData?.newsData
|
||||
|
||||
DataTable.use(DT)
|
||||
const dataTable = newsData?.sort(
|
||||
const dataTable = loaderData?.newsData?.sort(
|
||||
(a, b) => new Date(b.live_at).getTime() - new Date(a.live_at).getTime(),
|
||||
)
|
||||
const dataColumns = [
|
||||
@ -63,9 +62,12 @@ export const ContentsPage = () => {
|
||||
<div className="text-sm text-[#7C7C7C]">ID: {data.id.slice(0, 8)}</div>
|
||||
</div>
|
||||
),
|
||||
4: (value: TCategoryResponse[]) =>
|
||||
value.map((item) => item.name).join(', '),
|
||||
5: (value: TTagResponse[]) => value.map((item) => item.name).join(', '),
|
||||
4: (value: TCategoryResponse[]) => (
|
||||
<div className="text-xs">{value.map((item) => item.name).join(', ')}</div>
|
||||
),
|
||||
5: (value: TTagResponse[]) => (
|
||||
<div className="text-xs">{value.map((item) => item.name).join(', ')}</div>
|
||||
),
|
||||
6: (value: string) =>
|
||||
value ? (
|
||||
<div className="rounded-full bg-[#FFFCAF] px-2 text-center text-[#DBCA6E]">
|
||||
|
||||
@ -5,14 +5,13 @@ import { Link, useRouteLoaderData } from 'react-router'
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { UiTable } from '~/components/ui/table'
|
||||
import { TitleDashboard } from '~/components/ui/title-dashboard'
|
||||
import type { loader } from '~/routes/_admin.lg-admin._dashboard.tags._index'
|
||||
import type { loader } from '~/routes/_admin.lg-admin._dashboard'
|
||||
|
||||
export const TagsPage = () => {
|
||||
const loaderData = useRouteLoaderData<typeof loader>(
|
||||
'routes/_admin.lg-admin._dashboard.tags._index',
|
||||
'routes/_admin.lg-admin._dashboard',
|
||||
)
|
||||
const tagsData = loaderData?.tagsData
|
||||
const dataTable = tagsData
|
||||
const { tagsData: dataTable } = loaderData || {}
|
||||
|
||||
DataTable.use(DT)
|
||||
const dataColumns = [
|
||||
|
||||
@ -5,14 +5,14 @@ import { useFetcher, useNavigate, useRouteLoaderData } from 'react-router'
|
||||
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
|
||||
import { z } from 'zod'
|
||||
|
||||
import type { newsResponseSchema } from '~/apis/admin/get-news'
|
||||
import type { newsResponseSchema } from '~/apis/common/get-news'
|
||||
import { TextEditor } from '~/components/text-editor'
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { Combobox } from '~/components/ui/combobox'
|
||||
import { Input } from '~/components/ui/input'
|
||||
import { Switch } from '~/components/ui/switch'
|
||||
import { TitleDashboard } from '~/components/ui/title-dashboard'
|
||||
import type { loader } from '~/routes/_admin.lg-admin'
|
||||
import type { loader } from '~/routes/_admin.lg-admin._dashboard'
|
||||
|
||||
export const contentSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
@ -48,7 +48,9 @@ export const contentSchema = z.object({
|
||||
content: z.string().min(1, {
|
||||
message: 'Konten is required',
|
||||
}),
|
||||
featured_image: z.string().optional(),
|
||||
featured_image: z.string().url({
|
||||
message: 'Gambar Unggulan must be a valid URL',
|
||||
}),
|
||||
is_premium: z.boolean().optional(),
|
||||
live_at: z.string().min(1, {
|
||||
message: 'Tanggal live is required',
|
||||
@ -64,9 +66,11 @@ export const FormContentsPage = (properties: TProperties) => {
|
||||
const { newsData } = properties || {}
|
||||
const fetcher = useFetcher()
|
||||
const navigate = useNavigate()
|
||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_admin.lg-admin')
|
||||
const categories = loaderData?.categoriesData
|
||||
const tags = loaderData?.tagsData
|
||||
const loaderData = useRouteLoaderData<typeof loader>(
|
||||
'routes/_admin.lg-admin._dashboard',
|
||||
)
|
||||
const { categoriesData: categories } = loaderData || {}
|
||||
const { tagsData: tags } = loaderData || {}
|
||||
const [error, setError] = useState<string>()
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
|
||||
|
||||
@ -1,130 +0,0 @@
|
||||
import { DUMMY_DESCRIPTION } from '~/data/contents'
|
||||
import type { TNews } from '~/types/news'
|
||||
|
||||
export const BERITA: TNews = {
|
||||
title: 'BERITA',
|
||||
description: DUMMY_DESCRIPTION,
|
||||
items: [
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'Travelling as a way of self-discovery and progress',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-2.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
slug: 'travelling-as-a-way-of-self-discovery-and-progress',
|
||||
},
|
||||
{
|
||||
title: 'How does writing influence your personal brand?',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-3.jpg',
|
||||
tags: ['Hukum'],
|
||||
slug: 'how-does-writing-influence-your-personal-brand',
|
||||
},
|
||||
{
|
||||
title: 'Helping a local business reinvent itself',
|
||||
content:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.',
|
||||
featured: '/images/news-4.jpg',
|
||||
tags: ['Hukum Property'],
|
||||
isPremium: true,
|
||||
slug: 'helping-a-local-business-reinvent-itself',
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -1,18 +1,15 @@
|
||||
import { useParams, useRouteLoaderData } from 'react-router'
|
||||
import { useRouteLoaderData } from 'react-router'
|
||||
|
||||
import { Card } from '~/components/ui/card'
|
||||
import { CategorySection } from '~/components/ui/category-section'
|
||||
import type { loader } from '~/routes/_news'
|
||||
|
||||
import { BERITA } from './data'
|
||||
import type { loader } from '~/routes/_news.category.$code'
|
||||
|
||||
export const NewsCategoriesPage = () => {
|
||||
const parameters = useParams()
|
||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
|
||||
const { name, description } =
|
||||
loaderData?.categoriesData.find((item) => item.code === parameters.code) ||
|
||||
{}
|
||||
const { items } = BERITA
|
||||
const loaderData = useRouteLoaderData<typeof loader>(
|
||||
'routes/_news.category.$code',
|
||||
)
|
||||
const { categoryData, newsData } = loaderData || {}
|
||||
const { name, description } = categoryData || {}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@ -20,7 +17,7 @@ export const NewsCategoriesPage = () => {
|
||||
<CategorySection
|
||||
title={name || ''}
|
||||
description={description || ''}
|
||||
items={items}
|
||||
items={newsData || []}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -2,22 +2,26 @@ import htmlParse from 'html-react-parser'
|
||||
import { useReadingTime } from 'react-hook-reading-time'
|
||||
import { useRouteLoaderData } from 'react-router'
|
||||
|
||||
import type { TTagResponse } from '~/apis/common/get-tags'
|
||||
import { Card } from '~/components/ui/card'
|
||||
import { CarouselSection } from '~/components/ui/carousel-section'
|
||||
import { NewsAuthor } from '~/components/ui/news-author'
|
||||
import { SocialShareButtons } from '~/components/ui/social-share'
|
||||
import { BERITA } from '~/data/contents'
|
||||
import { Tags } from '~/components/ui/tags'
|
||||
import type { loader } from '~/routes/_news.detail.$slug'
|
||||
import type { TNews } from '~/types/news'
|
||||
|
||||
export const NewsDetailPage = () => {
|
||||
const loaderData = useRouteLoaderData<typeof loader>(
|
||||
'routes/_news.detail.$slug',
|
||||
)
|
||||
const berita: TNews = {
|
||||
title: loaderData?.beritaCategory?.name || '',
|
||||
description: loaderData?.beritaCategory?.description || '',
|
||||
items: loaderData?.beritaNews || [],
|
||||
}
|
||||
const currentUrl = globalThis.location
|
||||
const { newsDetailData } = loaderData || {}
|
||||
const { title, content, featured_image, author, live_at, tags } =
|
||||
newsDetailData || {}
|
||||
loaderData?.newsDetailData || {}
|
||||
|
||||
const { text } = useReadingTime(content || '')
|
||||
|
||||
@ -50,7 +54,7 @@ export const NewsDetailPage = () => {
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center justify-center">
|
||||
<article className="prose prose-stone">
|
||||
<article className="prose prose-headings:my-0.5 prose-p:my-0.5">
|
||||
{content && htmlParse(content)}
|
||||
</article>
|
||||
</div>
|
||||
@ -63,22 +67,14 @@ export const NewsDetailPage = () => {
|
||||
title={`${title}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
{tags?.map((tag: TTagResponse) => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="rounded bg-gray-300 p-1"
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Tags tags={tags || []} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white p-5 max-sm:hidden">
|
||||
<CarouselSection {...BERITA} />
|
||||
<CarouselSection {...berita} />
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,25 +1,45 @@
|
||||
import { useRouteLoaderData } from 'react-router'
|
||||
|
||||
import { Card } from '~/components/ui/card'
|
||||
import { CarouselHero } from '~/components/ui/carousel-hero'
|
||||
import { CarouselSection } from '~/components/ui/carousel-section'
|
||||
import { Newsletter } from '~/components/ui/newsletter'
|
||||
import { BERITA, SPOTLIGHT } from '~/data/contents'
|
||||
import type { loader } from '~/routes/_news._index'
|
||||
import type { TNews } from '~/types/news'
|
||||
|
||||
export const NewsPage = () => {
|
||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_news._index')
|
||||
const spotlight: TNews = {
|
||||
title: loaderData?.spotlightCategory?.name || '',
|
||||
description: loaderData?.spotlightCategory?.description || '',
|
||||
items: loaderData?.spotlightNews || [],
|
||||
}
|
||||
const berita: TNews = {
|
||||
title: loaderData?.beritaCategory?.name || '',
|
||||
description: loaderData?.beritaCategory?.description || '',
|
||||
items: loaderData?.beritaNews || [],
|
||||
}
|
||||
const kajian: TNews = {
|
||||
title: loaderData?.kajianCategory?.name || '',
|
||||
description: loaderData?.kajianCategory?.description || '',
|
||||
items: loaderData?.kajianNews || [],
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Card>
|
||||
<CarouselHero {...SPOTLIGHT} />
|
||||
<CarouselHero {...spotlight} />
|
||||
</Card>
|
||||
<div className="min-h-[400px] sm:min-h-[300px]">
|
||||
<Newsletter className="mr-0 sm:-ml-14" />
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CarouselSection {...BERITA} />
|
||||
<CarouselSection {...berita} />
|
||||
</Card>
|
||||
<Card>
|
||||
<CarouselSection {...kajian} />
|
||||
</Card>
|
||||
{/* <Card>
|
||||
<CarouselSection {...KAJIAN} />
|
||||
</Card> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,16 +1,4 @@
|
||||
import { getCategories } from '~/apis/common/get-categories'
|
||||
import { handleCookie } from '~/libs/cookies'
|
||||
import { CategoriesPage } from '~/pages/dashboard-categories'
|
||||
|
||||
import type { Route } from './+types/_admin.lg-admin._dashboard.categories._index'
|
||||
|
||||
export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
const { staffToken } = await handleCookie(request)
|
||||
const { data: dataCategories } = await getCategories({
|
||||
accessToken: staffToken,
|
||||
})
|
||||
return { dataCategories }
|
||||
}
|
||||
|
||||
const DashboardCategoriesIndexLayout = () => <CategoriesPage />
|
||||
export default DashboardCategoriesIndexLayout
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
import { getCategories } from '~/apis/common/get-categories'
|
||||
import { handleCookie } from '~/libs/cookies'
|
||||
import { FormCategoryPage } from '~/pages/form-category'
|
||||
|
||||
import type { Route } from './+types/_admin.lg-admin._dashboard.categories.update.$id'
|
||||
|
||||
export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
||||
const { staffToken } = await handleCookie(request)
|
||||
const { data: categoriesData } = await getCategories({
|
||||
accessToken: staffToken,
|
||||
})
|
||||
export const loader = async ({ params }: Route.LoaderArgs) => {
|
||||
const { data: categoriesData } = await getCategories()
|
||||
const categoryData = categoriesData.find(
|
||||
(category) => category.id === params.id,
|
||||
)
|
||||
@ -18,7 +14,7 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
||||
const DashboardCategoriesUpdateLayout = ({
|
||||
loaderData,
|
||||
}: Route.ComponentProps) => {
|
||||
const categoryData = loaderData.categoryData
|
||||
const { categoryData } = loaderData || {}
|
||||
return <FormCategoryPage categoryData={categoryData} />
|
||||
}
|
||||
export default DashboardCategoriesUpdateLayout
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
import { getNews } from '~/apis/admin/get-news'
|
||||
import { handleCookie } from '~/libs/cookies'
|
||||
import { getNews } from '~/apis/common/get-news'
|
||||
import { ContentsPage } from '~/pages/dashboard-contents'
|
||||
|
||||
import type { Route } from './+types/_admin.lg-admin._dashboard.contents._index'
|
||||
|
||||
export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
const { staffToken } = await handleCookie(request)
|
||||
const { data: newsData } = await getNews({
|
||||
accessToken: staffToken,
|
||||
})
|
||||
export const loader = async ({}: Route.LoaderArgs) => {
|
||||
const { data: newsData } = await getNews()
|
||||
return { newsData }
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
||||
}
|
||||
|
||||
const DashboardContentUpdateLayout = ({ loaderData }: Route.ComponentProps) => {
|
||||
const newsData = loaderData.newsData
|
||||
const { newsData } = loaderData || {}
|
||||
return <FormContentsPage newsData={newsData} />
|
||||
}
|
||||
export default DashboardContentUpdateLayout
|
||||
|
||||
@ -1,15 +1,4 @@
|
||||
import { getTags } from '~/apis/common/get-tags'
|
||||
import { handleCookie } from '~/libs/cookies'
|
||||
import { TagsPage } from '~/pages/dashboard-tags'
|
||||
|
||||
import type { Route } from './+types/_admin.lg-admin._dashboard.tags._index'
|
||||
export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
const { staffToken } = await handleCookie(request)
|
||||
const { data: tagsData } = await getTags({
|
||||
accessToken: staffToken,
|
||||
})
|
||||
return { tagsData }
|
||||
}
|
||||
|
||||
const DashboardTagsIndexLayout = () => <TagsPage />
|
||||
export default DashboardTagsIndexLayout
|
||||
|
||||
@ -1,20 +1,16 @@
|
||||
import { getTags } from '~/apis/common/get-tags'
|
||||
import { handleCookie } from '~/libs/cookies'
|
||||
import { FormTagPage } from '~/pages/form-tag'
|
||||
|
||||
import type { Route } from './+types/_admin.lg-admin._dashboard.tags.update.$id'
|
||||
|
||||
export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
||||
const { staffToken } = await handleCookie(request)
|
||||
const { data: tagsData } = await getTags({
|
||||
accessToken: staffToken,
|
||||
})
|
||||
export const loader = async ({ params }: Route.LoaderArgs) => {
|
||||
const { data: tagsData } = await getTags()
|
||||
const tagData = tagsData.find((tag) => tag.id === params.id)
|
||||
return { tagData }
|
||||
}
|
||||
|
||||
const DashboardTagUpdateLayout = ({ loaderData }: Route.ComponentProps) => {
|
||||
const tagData = loaderData.tagData
|
||||
const { tagData } = loaderData || {}
|
||||
return <FormTagPage tagData={tagData} />
|
||||
}
|
||||
export default DashboardTagUpdateLayout
|
||||
|
||||
@ -1,7 +1,21 @@
|
||||
import { Outlet } from 'react-router'
|
||||
|
||||
import { getCategories } from '~/apis/common/get-categories'
|
||||
import { getTags } from '~/apis/common/get-tags'
|
||||
import { AdminDashboardLayout } from '~/layouts/admin/dashboard'
|
||||
|
||||
import type { Route } from './+types/_admin.lg-admin._dashboard'
|
||||
|
||||
export const loader = async ({}: Route.LoaderArgs) => {
|
||||
const { data: categoriesData } = await getCategories()
|
||||
const { data: tagsData } = await getTags()
|
||||
|
||||
return {
|
||||
categoriesData,
|
||||
tagsData,
|
||||
}
|
||||
}
|
||||
|
||||
const DashboardLayout = () => {
|
||||
return (
|
||||
<AdminDashboardLayout>
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { Outlet, redirect } from 'react-router'
|
||||
|
||||
import { getStaff } from '~/apis/admin/get-staff'
|
||||
import { getCategories } from '~/apis/common/get-categories'
|
||||
import { getTags } from '~/apis/common/get-tags'
|
||||
import { AUTH_PAGES } from '~/configs/pages'
|
||||
import { AdminDefaultLayout } from '~/layouts/admin/default'
|
||||
import { handleCookie } from '~/libs/cookies'
|
||||
@ -29,13 +27,9 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
|
||||
})
|
||||
staffData = data
|
||||
}
|
||||
const { data: categoriesData } = await getCategories()
|
||||
const { data: tagsData } = await getTags()
|
||||
|
||||
return {
|
||||
staffData,
|
||||
categoriesData,
|
||||
tagsData,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,40 @@
|
||||
import { getCategories } from '~/apis/common/get-categories'
|
||||
import { getNews } from '~/apis/common/get-news'
|
||||
import { NewsPage } from '~/pages/news'
|
||||
|
||||
import type { Route } from './+types/_news._index'
|
||||
|
||||
export const loader = async ({}: Route.LoaderArgs) => {
|
||||
const { data: categoriesData } = await getCategories()
|
||||
|
||||
const spotlightCode = 'spotlight'
|
||||
const spotlightCategory = categoriesData.find(
|
||||
(category) => category.code === spotlightCode,
|
||||
)
|
||||
const { data: spotlightNews } = await getNews({ categories: [spotlightCode] })
|
||||
|
||||
const beritaCode = 'berita'
|
||||
const beritaCategory = categoriesData.find(
|
||||
(category) => category.code === beritaCode,
|
||||
)
|
||||
const { data: beritaNews } = await getNews({ categories: [beritaCode] })
|
||||
|
||||
const kajianCode = 'kajian'
|
||||
const kajianCategory = categoriesData.find(
|
||||
(category) => category.code === kajianCode,
|
||||
)
|
||||
const { data: kajianNews } = await getNews({ categories: [kajianCode] })
|
||||
|
||||
return {
|
||||
spotlightCategory,
|
||||
spotlightNews,
|
||||
beritaCategory,
|
||||
beritaNews,
|
||||
kajianCategory,
|
||||
kajianNews,
|
||||
}
|
||||
}
|
||||
|
||||
const NewsIndexLayout = () => <NewsPage />
|
||||
|
||||
export default NewsIndexLayout
|
||||
|
||||
@ -1,5 +1,31 @@
|
||||
import { getCategories } from '~/apis/common/get-categories'
|
||||
import { getNews } from '~/apis/common/get-news'
|
||||
import { APP } from '~/configs/meta'
|
||||
import { NewsCategoriesPage } from '~/pages/news-categories'
|
||||
|
||||
import type { Route } from './+types/_news.category.$code'
|
||||
|
||||
export const loader = async ({ params }: Route.LoaderArgs) => {
|
||||
const { data: categoriesData } = await getCategories()
|
||||
const categoryData = categoriesData.find(
|
||||
(category) => category.code === params.code,
|
||||
)
|
||||
const { data: newsData } = await getNews({ categories: [params.code] })
|
||||
return { categoryData, newsData }
|
||||
}
|
||||
|
||||
export const meta = ({ data }: Route.MetaArgs) => {
|
||||
const { categoryData } = data
|
||||
const metaTitle = APP.title
|
||||
const title = `${categoryData?.name} - ${metaTitle}`
|
||||
|
||||
return [
|
||||
{
|
||||
title,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const NewsCategoriesLayout = () => <NewsCategoriesPage />
|
||||
|
||||
export default NewsCategoriesLayout
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { getCategories } from '~/apis/common/get-categories'
|
||||
import { getNews } from '~/apis/common/get-news'
|
||||
import { getNewsBySlug } from '~/apis/common/get-news-by-slug'
|
||||
import { getUser } from '~/apis/news/get-user'
|
||||
import { APP } from '~/configs/meta'
|
||||
import { handleCookie } from '~/libs/cookies'
|
||||
import { NewsDetailPage } from '~/pages/news-detail'
|
||||
@ -8,22 +9,21 @@ import type { Route } from './+types/_news.detail.$slug'
|
||||
|
||||
export const loader = async ({ request, params }: Route.LoaderArgs) => {
|
||||
const { userToken } = await handleCookie(request)
|
||||
let userData
|
||||
if (userToken) {
|
||||
const { data } = await getUser({
|
||||
accessToken: userToken,
|
||||
})
|
||||
userData = data
|
||||
}
|
||||
// TODO: need handle if user not access non premium data
|
||||
const { data: newsDetailData } = await getNewsBySlug({
|
||||
slug: params.slug,
|
||||
accessToken: userToken,
|
||||
})
|
||||
const { data: categoriesData } = await getCategories()
|
||||
const beritaCode = 'berita'
|
||||
const beritaCategory = categoriesData.find(
|
||||
(category) => category.code === beritaCode,
|
||||
)
|
||||
const { data: beritaNews } = await getNews({ categories: [beritaCode] })
|
||||
|
||||
return {
|
||||
newsDetailData,
|
||||
userData,
|
||||
beritaCategory,
|
||||
beritaNews,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +1,7 @@
|
||||
import type { TNewsResponse } from '~/apis/common/get-news'
|
||||
|
||||
export type TNews = {
|
||||
title: string
|
||||
description: string
|
||||
items: Pick<
|
||||
TNewsDetail,
|
||||
'title' | 'content' | 'featured' | 'slug' | 'tags' | 'isPremium'
|
||||
>[]
|
||||
}
|
||||
|
||||
type TNewsDetail = {
|
||||
title: string
|
||||
content: string
|
||||
featured: string
|
||||
author: string
|
||||
date: Date
|
||||
slug: string
|
||||
tags?: Array<string>
|
||||
isPremium?: boolean
|
||||
categories?: Array<string>
|
||||
items: TNewsResponse[]
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"datatables.net-dt": "^2.2.2",
|
||||
"datatables.net-react": "^1.0.0",
|
||||
"embla-carousel-autoplay": "^8.5.2",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"html-react-parser": "^5.2.2",
|
||||
"isbot": "^5.1.17",
|
||||
@ -46,6 +47,7 @@
|
||||
"react-router": "^7.1.3",
|
||||
"react-share": "^5.2.2",
|
||||
"remix-hook-form": "^6.1.3",
|
||||
"string-strip-html": "^13.4.12",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"xior": "^0.6.3",
|
||||
"zod": "^3.24.2"
|
||||
|
||||
115
pnpm-lock.yaml
generated
115
pnpm-lock.yaml
generated
@ -65,6 +65,9 @@ importers:
|
||||
datatables.net-react:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
embla-carousel-autoplay:
|
||||
specifier: ^8.5.2
|
||||
version: 8.5.2(embla-carousel@8.5.2)
|
||||
embla-carousel-react:
|
||||
specifier: ^8.5.2
|
||||
version: 8.5.2(react@19.0.0)
|
||||
@ -104,6 +107,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)
|
||||
string-strip-html:
|
||||
specifier: ^13.4.12
|
||||
version: 13.4.12
|
||||
tailwind-merge:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
@ -1653,6 +1659,9 @@ packages:
|
||||
'@types/linkify-it@5.0.0':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
'@types/lodash-es@4.17.12':
|
||||
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
|
||||
|
||||
'@types/lodash@4.17.16':
|
||||
resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==}
|
||||
|
||||
@ -1993,6 +2002,10 @@ packages:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
codsen-utils@1.6.7:
|
||||
resolution: {integrity: sha512-M+9D3IhFAk4T8iATX62herVuIx1sp5kskWgxEegKD/JwTTSSGjGQs5Q5J4vVJ4mLcn1uhfxDYv6Yzr8zleHF3w==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@ -2321,6 +2334,11 @@ packages:
|
||||
electron-to-chromium@1.5.90:
|
||||
resolution: {integrity: sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==}
|
||||
|
||||
embla-carousel-autoplay@8.5.2:
|
||||
resolution: {integrity: sha512-27emJ0px3q/c0kCHCjwRrEbYcyYUPfGO3g5IBWF1i7714TTzE6L9P81V6PHLoSMAKJ1aHoT2e7YFOsuFKCbyag==}
|
||||
peerDependencies:
|
||||
embla-carousel: 8.5.2
|
||||
|
||||
embla-carousel-react@8.5.2:
|
||||
resolution: {integrity: sha512-Tmx+uY3MqseIGdwp0ScyUuxpBgx5jX1f7od4Cm5mDwg/dptEiTKf9xp6tw0lZN2VA9JbnVMl/aikmbc53c6QFA==}
|
||||
peerDependencies:
|
||||
@ -2827,6 +2845,9 @@ packages:
|
||||
html-dom-parser@5.0.13:
|
||||
resolution: {integrity: sha512-B7JonBuAfG32I7fDouUQEogBrz3jK9gAuN1r1AaXpED6dIhtg/JwiSRhjGL7aOJwRz3HU4efowCjQBaoXiREqg==}
|
||||
|
||||
html-entities@2.5.2:
|
||||
resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==}
|
||||
|
||||
html-react-parser@5.2.2:
|
||||
resolution: {integrity: sha512-yA5012CJGSFWYZsgYzfr6HXJgDap38/AEP4ra8Cw+WHIi2ZRDXRX/QVYdumRf1P8zKyScKd6YOrWYvVEiPfGKg==}
|
||||
peerDependencies:
|
||||
@ -3272,6 +3293,9 @@ packages:
|
||||
resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
lodash-es@4.17.21:
|
||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||
|
||||
lodash.camelcase@4.3.0:
|
||||
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
|
||||
|
||||
@ -3857,6 +3881,22 @@ packages:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
ranges-apply@7.0.19:
|
||||
resolution: {integrity: sha512-imA03KuTSuSpQtq9SDhavUz7BtiddCPj+fsYM/XpdypRN/s8vyTayKzni6m5nYs7VMds1kSNK1V3jfwVrPUWBQ==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
ranges-merge@9.0.18:
|
||||
resolution: {integrity: sha512-2+6Eh4yxi5sudUmvCdvxVOSdXIXV+Brfutw8chhZmqkT0REqlzilpyQps1S5n8c7f0+idblqSAHGahTbf/Ar5g==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
ranges-push@7.0.18:
|
||||
resolution: {integrity: sha512-wzGHipEklSlY0QloQ88PNt+PkTURIB42PLLcQGY+WyYBlNpnrzps6EYooD3RqNXtdqMQ9kR8IVaF9itRYtuzLA==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
ranges-sort@6.0.13:
|
||||
resolution: {integrity: sha512-M3P0/dUnU3ihLPX2jq0MT2NJA1ls/q6cUAUVPD28xdFFqm3VFarPjTKKhnsBSvYCpZD8HdiElAGAyoPu6uOQjA==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
raw-body@2.5.2:
|
||||
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -4246,6 +4286,22 @@ packages:
|
||||
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
||||
engines: {node: '>=0.6.19'}
|
||||
|
||||
string-collapse-leading-whitespace@7.0.9:
|
||||
resolution: {integrity: sha512-lEuTHlogBT9PWipfk0FOyvoMKX8syiE03QoFk5MDh8oS0AJ2C07IlstR5cGkxz48nKkOIuvkC28w9Rx/cVRNDg==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
string-left-right@6.0.20:
|
||||
resolution: {integrity: sha512-dz2mUgmsI7m/FMe+BoxZ2+73X1TUoQvjCdnq8vbIAnHlvWfVZleNUR+lw+QgHA2dlJig+hUWC9bFYdNFGGy2bA==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
string-strip-html@13.4.12:
|
||||
resolution: {integrity: sha512-mr1GM1TFcwDkYwLE7TNkHY+Lf3YFEBa19W9KntZoJJSbrKF07W4xmLkPnqf8cypEGyr+dc1H9hsdTw5VSNVGxg==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
string-trim-spaces-only@5.0.12:
|
||||
resolution: {integrity: sha512-Un5nIO1av+hzfnKGmY+bWe0AD4WH37TuDW+jeMPm81rUvU2r3VPRj9vEKdZkPmuhYAMuKlzarm7jDSKwJKOcpQ==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
@ -4358,6 +4414,9 @@ packages:
|
||||
through@2.3.8:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
tiny-lru@11.2.11:
|
||||
resolution: {integrity: sha512-27BIW0dIWTYYoWNnqSmoNMKe5WIbkXsc0xaCQHd3/3xT2XMuMJrzHdrO9QBFR14emBz1Bu0dOAs2sCBBrvgPQA==}
|
||||
engines: {node: '>=12'}
|
||||
@ -6216,6 +6275,10 @@ snapshots:
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/lodash-es@4.17.12':
|
||||
dependencies:
|
||||
'@types/lodash': 4.17.16
|
||||
|
||||
'@types/lodash@4.17.16': {}
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
@ -6615,6 +6678,10 @@ snapshots:
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
codsen-utils@1.6.7:
|
||||
dependencies:
|
||||
rfdc: 1.4.1
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
@ -6931,6 +6998,10 @@ snapshots:
|
||||
|
||||
electron-to-chromium@1.5.90: {}
|
||||
|
||||
embla-carousel-autoplay@8.5.2(embla-carousel@8.5.2):
|
||||
dependencies:
|
||||
embla-carousel: 8.5.2
|
||||
|
||||
embla-carousel-react@8.5.2(react@19.0.0):
|
||||
dependencies:
|
||||
embla-carousel: 8.5.2
|
||||
@ -7632,6 +7703,8 @@ snapshots:
|
||||
domhandler: 5.0.3
|
||||
htmlparser2: 10.0.0
|
||||
|
||||
html-entities@2.5.2: {}
|
||||
|
||||
html-react-parser@5.2.2(@types/react@19.0.8)(react@19.0.0):
|
||||
dependencies:
|
||||
domhandler: 5.0.3
|
||||
@ -8061,6 +8134,8 @@ snapshots:
|
||||
dependencies:
|
||||
p-locate: 6.0.0
|
||||
|
||||
lodash-es@4.17.21: {}
|
||||
|
||||
lodash.camelcase@4.3.0: {}
|
||||
|
||||
lodash.castarray@4.4.0: {}
|
||||
@ -8586,6 +8661,25 @@ snapshots:
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
ranges-apply@7.0.19:
|
||||
dependencies:
|
||||
ranges-merge: 9.0.18
|
||||
tiny-invariant: 1.3.3
|
||||
|
||||
ranges-merge@9.0.18:
|
||||
dependencies:
|
||||
ranges-push: 7.0.18
|
||||
ranges-sort: 6.0.13
|
||||
|
||||
ranges-push@7.0.18:
|
||||
dependencies:
|
||||
codsen-utils: 1.6.7
|
||||
ranges-sort: 6.0.13
|
||||
string-collapse-leading-whitespace: 7.0.9
|
||||
string-trim-spaces-only: 5.0.12
|
||||
|
||||
ranges-sort@6.0.13: {}
|
||||
|
||||
raw-body@2.5.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@ -9045,6 +9139,25 @@ snapshots:
|
||||
|
||||
string-argv@0.3.2: {}
|
||||
|
||||
string-collapse-leading-whitespace@7.0.9: {}
|
||||
|
||||
string-left-right@6.0.20:
|
||||
dependencies:
|
||||
codsen-utils: 1.6.7
|
||||
rfdc: 1.4.1
|
||||
|
||||
string-strip-html@13.4.12:
|
||||
dependencies:
|
||||
'@types/lodash-es': 4.17.12
|
||||
codsen-utils: 1.6.7
|
||||
html-entities: 2.5.2
|
||||
lodash-es: 4.17.21
|
||||
ranges-apply: 7.0.19
|
||||
ranges-push: 7.0.18
|
||||
string-left-right: 6.0.20
|
||||
|
||||
string-trim-spaces-only@5.0.12: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
@ -9174,6 +9287,8 @@ snapshots:
|
||||
|
||||
through@2.3.8: {}
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tiny-lru@11.2.11: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user