Compare commits

...

19 Commits

Author SHA1 Message Date
Ardeman
e7eda086e4 refactor: integrate autoplay functionality in Banner and enhance news data handling 2025-03-09 14:32:04 +08:00
Ardeman
550b16cfd3 refactor: update display of categories and tags with improved text size 2025-03-09 13:44:04 +08:00
Ardeman
0b3217407c refactor: adjust Navbar styles for improved button ring and text size 2025-03-09 13:39:52 +08:00
Ardeman
7afabdaa03 refactor: update news types and enhance news data handling in components 2025-03-09 12:32:36 +08:00
Ardeman
9f82779456 refactor: adjust spacing in CarouselSection and CategorySection for improved layout 2025-03-09 12:00:10 +08:00
Ardeman
ee816b8db7 refactor: implement Tags component for improved tag display and update news types 2025-03-09 11:56:57 +08:00
Ardeman
1b6321f2bd refactor: update tag styling for improved visibility and consistency 2025-03-09 11:39:14 +08:00
Ardeman
4847ef2be3 refactor: enhance news handling and improve content display in various components 2025-03-09 11:36:47 +08:00
Ardeman
bad5294030 refactor: remove accessToken from parameters in API requests for cleaner handling 2025-03-09 10:36:38 +08:00
Ardeman
9976626f97 refactor: remove accessToken from parameters in API requests for cleaner handling 2025-03-09 10:34:25 +08:00
Ardeman
d38bf3a705 refactor: reorganize API imports and simplify parameter handling in various components 2025-03-09 10:23:11 +08:00
Ardeman
687e3c8d01 refactor: simplify loader function by removing cookie handling in tag update route 2025-03-09 09:59:55 +08:00
Ardeman
4554cca392 refactor: update loaderData imports in dashboard pages for consistency 2025-03-09 09:57:26 +08:00
Ardeman
05b3af8718 fix: update category selection logic to use code instead of id 2025-03-09 09:50:18 +08:00
Ardeman
e2e3095c33 refactor: update loaderData usage in NewsCategoriesPage for improved clarity 2025-03-09 09:49:06 +08:00
Ardeman
5b94c6df91 refactor: simplify loaderData usage in dashboard categories and contents pages 2025-03-09 09:44:41 +08:00
Ardeman
634c809489 fix: allow end_date to be nullable in user response schema 2025-03-09 09:41:11 +08:00
Ardeman
0da2006e78 refactor: destructure loaderData in multiple components for cleaner code 2025-03-09 09:32:51 +08:00
Ardeman
09e5f84b8b style: update social share button styles for improved UI consistency 2025-03-09 09:09:54 +08:00
51 changed files with 502 additions and 523 deletions

View File

@ -1,6 +1,6 @@
import { z } from 'zod' 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' import type { TCategorySchema } from '~/pages/form-category'
const categoryResponseSchema = z.object({ const categoryResponseSchema = z.object({
@ -10,14 +10,13 @@ const categoryResponseSchema = z.object({
}) })
type TParameters = { type TParameters = {
accessToken: string
payload: TCategorySchema payload: TCategorySchema
} } & THttpServer
export const createCategoryRequest = async (parameters: TParameters) => { export const createCategoryRequest = async (parameters: TParameters) => {
const { accessToken, payload } = parameters const { payload, ...restParameters } = parameters
try { try {
const { data } = await HttpServer({ accessToken }).post( const { data } = await HttpServer(restParameters).post(
'/api/category/create', '/api/category/create',
payload, payload,
) )

View File

@ -1,6 +1,6 @@
import { z } from 'zod' 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' import type { TContentSchema } from '~/pages/form-contents'
const newsResponseSchema = z.object({ const newsResponseSchema = z.object({
@ -10,21 +10,20 @@ const newsResponseSchema = z.object({
}) })
type TParameter = { type TParameter = {
accessToken: string
payload: TContentSchema payload: TContentSchema
} } & THttpServer
export const createNewsRequest = async (parameters: TParameter) => { 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 { try {
const { categories, tags, ...restPayload } = payload const { data } = await HttpServer(restParameters).post(
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(
'/api/news/create', '/api/news/create',
transformedPayload, transformedPayload,
) )

View File

@ -1,6 +1,6 @@
import { z } from 'zod' 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' import type { TTagSchema } from '~/pages/form-tag'
const tagsResponseSchema = z.object({ const tagsResponseSchema = z.object({
@ -10,21 +10,15 @@ const tagsResponseSchema = z.object({
}) })
type TParameters = { type TParameters = {
accessToken: string
payload: TTagSchema payload: TTagSchema
} } & THttpServer
export const createTagsRequest = async (parameters: TParameters) => { export const createTagsRequest = async (parameters: TParameters) => {
const { accessToken, payload } = parameters const { payload, ...restParameters } = parameters
try { try {
const { ...restPayload } = payload const { data } = await HttpServer(restParameters).post(
const transformedPayload = {
...restPayload,
}
const { data } = await HttpServer({ accessToken }).post(
'/api/tag/create', '/api/tag/create',
transformedPayload, payload,
) )
return tagsResponseSchema.parse(data) return tagsResponseSchema.parse(data)
} catch (error) { } catch (error) {

View File

@ -1,6 +1,6 @@
import { z } from 'zod' 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' import type { TCategorySchema } from '~/pages/form-category'
const categoryResponseSchema = z.object({ const categoryResponseSchema = z.object({
@ -10,15 +10,14 @@ const categoryResponseSchema = z.object({
}) })
type TParameters = { type TParameters = {
accessToken: string
payload: TCategorySchema payload: TCategorySchema
} } & THttpServer
export const updateCategoryRequest = async (parameters: TParameters) => { export const updateCategoryRequest = async (parameters: TParameters) => {
const { accessToken, payload } = parameters const { payload, ...restParameters } = parameters
const { id, ...restPayload } = payload
try { try {
const { id, ...restPayload } = payload const { data } = await HttpServer(restParameters).put(
const { data } = await HttpServer({ accessToken }).put(
`/api/category/${id}/update`, `/api/category/${id}/update`,
restPayload, restPayload,
) )

View File

@ -1,6 +1,6 @@
import { z } from 'zod' 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' import type { TContentSchema } from '~/pages/form-contents'
const newsResponseSchema = z.object({ const newsResponseSchema = z.object({
@ -10,21 +10,20 @@ const newsResponseSchema = z.object({
}) })
type TParameter = { type TParameter = {
accessToken: string
payload: TContentSchema payload: TContentSchema
} } & THttpServer
export const updateNewsRequest = async (parameters: TParameter) => { 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 { try {
const { categories, tags, id, ...restPayload } = payload const { data } = await HttpServer(restParameters).put(
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(
`/api/news/${id}/update`, `/api/news/${id}/update`,
transformedPayload, transformedPayload,
) )

View File

@ -1,6 +1,6 @@
import { z } from 'zod' 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' import type { TTagSchema } from '~/pages/form-tag'
const tagResponseSchema = z.object({ const tagResponseSchema = z.object({
@ -10,15 +10,14 @@ const tagResponseSchema = z.object({
}) })
type TParameters = { type TParameters = {
accessToken: string
payload: TTagSchema payload: TTagSchema
} } & THttpServer
export const updateTagRequest = async (parameters: TParameters) => { export const updateTagRequest = async (parameters: TParameters) => {
const { accessToken, payload } = parameters const { payload, ...restParameters } = parameters
const { id, ...restPayload } = payload
try { try {
const { id, ...restPayload } = payload const { data } = await HttpServer(restParameters).put(
const { data } = await HttpServer({ accessToken }).put(
`/api/tag/${id}/update`, `/api/tag/${id}/update`,
restPayload, restPayload,
) )

View File

@ -1,6 +1,6 @@
import { z } from 'zod' 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' import { HttpServer, type THttpServer } from '~/libs/http-server'
const dataResponseSchema = z.object({ const dataResponseSchema = z.object({
@ -12,9 +12,9 @@ type TParameters = {
} & THttpServer } & THttpServer
export const getNewsBySlug = async (parameters: TParameters) => { export const getNewsBySlug = async (parameters: TParameters) => {
const { slug, accessToken } = parameters const { slug, ...restParameters } = parameters
try { try {
const { data } = await HttpServer({ accessToken }).get(`/api/news/${slug}`) const { data } = await HttpServer(restParameters).get(`/api/news/${slug}`)
return dataResponseSchema.parse(data) return dataResponseSchema.parse(data)
} catch (error) { } catch (error) {
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject

View File

@ -30,10 +30,20 @@ const dataResponseSchema = z.object({
export type TNewsResponse = z.infer<typeof newsResponseSchema> export type TNewsResponse = z.infer<typeof newsResponseSchema>
export type TAuthor = z.infer<typeof authorSchema> 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 { 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) return dataResponseSchema.parse(data)
} catch (error) { } catch (error) {
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject

View File

@ -11,7 +11,7 @@ const userResponseSchema = z.object({
id: z.string(), id: z.string(),
subscribe_plan_id: z.string(), subscribe_plan_id: z.string(),
start_date: z.string(), start_date: z.string(),
end_date: z.string(), end_date: z.string().nullable(),
status: z.string(), status: z.string(),
auto_renew: z.boolean(), auto_renew: z.boolean(),
subscribe_plan: z.object({ subscribe_plan: z.object({

View File

@ -4,12 +4,12 @@ import { HttpServer } from '~/libs/http-server'
import { loginResponseSchema } from './login-user' import { loginResponseSchema } from './login-user'
export const userRegisterRequest = async (payload: TRegisterSchema) => { export const userRegisterRequest = async (payload: TRegisterSchema) => {
const { subscribe_plan, ...restPayload } = payload
const transformedPayload = {
...restPayload,
subscribe_plan_id: subscribe_plan.id,
}
try { try {
const { subscribe_plan, ...restPayload } = payload
const transformedPayload = {
...restPayload,
subscribe_plan_id: subscribe_plan.id,
}
const { data } = await HttpServer().post( const { data } = await HttpServer().post(
'/api/user/register', '/api/user/register',
transformedPayload, transformedPayload,

View File

@ -23,6 +23,10 @@ body {
@apply outline-none; @apply outline-none;
} }
.ProseMirror-trailingBreak {
@apply hidden;
}
table.dataTable thead > tr { table.dataTable thead > tr {
border-bottom: 2px solid #c2c2c2; border-bottom: 2px solid #c2c2c2;
} }

View File

@ -36,7 +36,7 @@ const DESCRIPTIONS: DescriptionMap = {
export const SuccessModal = ({ isOpen, onClose }: ModalProperties) => { export const SuccessModal = ({ isOpen, onClose }: ModalProperties) => {
const { setIsLoginOpen, setIsSubscribeOpen } = useNewsContext() const { setIsLoginOpen, setIsSubscribeOpen } = useNewsContext()
const loaderData = useRouteLoaderData<typeof loader>('routes/_news') const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
const userData = loaderData?.userData const { userData } = loaderData || {}
const message = isOpen const message = isOpen
? DESCRIPTIONS[isOpen] ? DESCRIPTIONS[isOpen]

View File

@ -67,7 +67,13 @@ export const TextEditor = <TFormValues extends Record<string, unknown>>(
const editor = useEditor({ const editor = useEditor({
editable: !disabled, editable: !disabled,
extensions: [ extensions: [
StarterKit, StarterKit.configure({
paragraph: {
HTMLAttributes: {
class: 'min-h-1',
},
},
}),
Highlight, Highlight,
Image.configure({ Image.configure({
inline: true, inline: true,
@ -134,7 +140,7 @@ export const TextEditor = <TFormValues extends Record<string, unknown>>(
editor={editor} editor={editor}
id={id ?? generatedId} id={id ?? generatedId}
className={twMerge( 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, inputClassName,
)} )}
onClick={() => editor?.commands.focus()} onClick={() => editor?.commands.focus()}

View File

@ -1,10 +1,11 @@
import Autoplay from 'embla-carousel-autoplay'
import useEmblaCarousel from 'embla-carousel-react' import useEmblaCarousel from 'embla-carousel-react'
import { Link } from 'react-router' import { Link } from 'react-router'
import { BANNER } from '~/data/contents' import { BANNER } from '~/data/contents'
export const Banner = () => { export const Banner = () => {
const [emblaReference] = useEmblaCarousel({ loop: false }) const [emblaReference] = useEmblaCarousel({ loop: true }, [Autoplay()])
return ( return (
<div className=""> <div className="">

View File

@ -3,11 +3,11 @@ import type { ComponentProps, FC, PropsWithChildren } from 'react'
type TProperties = PropsWithChildren<ComponentProps<'div'>> type TProperties = PropsWithChildren<ComponentProps<'div'>>
export const Card: FC<TProperties> = (properties) => { export const Card: FC<TProperties> = (properties) => {
const { children, ...rest } = properties const { children, ...restProperties } = properties
return ( return (
<div <div
className="border-[.2px] border-black/20 bg-white p-[30px]" className="border-[.2px] border-black/20 bg-white p-[30px]"
{...rest} {...restProperties}
> >
{children} {children}
</div> </div>

View File

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

View File

@ -1,6 +1,7 @@
import useEmblaCarousel from 'embla-carousel-react' import useEmblaCarousel from 'embla-carousel-react'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useRouteLoaderData } from 'react-router' import { useRouteLoaderData } from 'react-router'
import { stripHtml } from 'string-strip-html'
import { Button } from '~/components/ui/button' import { Button } from '~/components/ui/button'
import { CarouselButton } from '~/components/ui/button-slide' import { CarouselButton } from '~/components/ui/button-slide'
@ -9,10 +10,12 @@ import type { loader } from '~/routes/_news'
import type { TNews } from '~/types/news' import type { TNews } from '~/types/news'
import { getPremiumAttribute } from '~/utils/render' import { getPremiumAttribute } from '~/utils/render'
import { Tags } from './tags'
export const CarouselSection = (properties: TNews) => { export const CarouselSection = (properties: TNews) => {
const { setIsSuccessOpen } = useNewsContext() const { setIsSuccessOpen } = useNewsContext()
const loaderData = useRouteLoaderData<typeof loader>('routes/_news') const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
const userData = loaderData?.userData const { userData } = loaderData || {}
const { title, description, items } = properties const { title, description, items } = properties
const [emblaReference, emblaApi] = useEmblaCarousel({ const [emblaReference, emblaApi] = useEmblaCarousel({
loop: false, 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"> <div className="embla__container col-span-3 flex max-h-[586px] sm:gap-x-8">
{items.map( {items.map(
({ featured, title, content, tags, slug, isPremium }, index) => ( (
{ featured_image, title, content, tags, slug, is_premium },
index,
) => (
<div <div
className="embla__slide w-full min-w-0 flex-none sm:w-1/3" className="embla__slide w-full min-w-0 flex-none sm:w-1/3"
key={index} key={index}
> >
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between gap-3">
<img <img
className="aspect-[174/100] max-h-[280px] w-full rounded-md object-cover sm:aspect-[5/4]" className="aspect-[174/100] max-h-[280px] w-full rounded-md object-cover sm:aspect-[5/4]"
src={featured} src={featured_image}
alt={title} alt={title}
/> />
<div className={'flex flex-col justify-between gap-4'}> <div className={'flex flex-col justify-between gap-4'}>
<div className={'flex text-sm uppercase'}> <Tags
{tags?.map((item) => ( tags={tags || []}
<span is_premium={is_premium}
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>
<div> <div>
<h3 className="mt-2 w-full text-xl font-bold sm:text-2xl lg:mt-0"> <h3 className="mt-2 w-full text-xl font-bold sm:text-2xl lg:mt-0">
{title} {title}
</h3> </h3>
<p className="text-md mt-5 text-[#777777] sm:text-xl"> <p className="text-md mt-5 line-clamp-3 text-[#777777] sm:text-xl">
{content} {stripHtml(content).result}
</p> </p>
</div> </div>
<Button <Button
size="block" size="block"
{...getPremiumAttribute({ {...getPremiumAttribute({
isPremium, isPremium: is_premium,
slug, slug,
onClick: () => setIsSuccessOpen('warning'), onClick: () => setIsSuccessOpen('warning'),
userData, userData,

View File

@ -1,18 +1,27 @@
import { useRouteLoaderData } from 'react-router' import { useRouteLoaderData } from 'react-router'
import { stripHtml } from 'string-strip-html'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import type { TNewsResponse } from '~/apis/common/get-news'
import { CarouselNextIcon } from '~/components/icons/carousel-next' import { CarouselNextIcon } from '~/components/icons/carousel-next'
import { CarouselPreviousIcon } from '~/components/icons/carousel-previous' import { CarouselPreviousIcon } from '~/components/icons/carousel-previous'
import { Button } from '~/components/ui/button' import { Button } from '~/components/ui/button'
import { useNewsContext } from '~/contexts/news' import { useNewsContext } from '~/contexts/news'
import type { loader } from '~/routes/_news' import type { loader } from '~/routes/_news'
import type { TNews } from '~/types/news'
import { getPremiumAttribute } from '~/utils/render' import { getPremiumAttribute } from '~/utils/render'
import { Tags } from './tags'
type TNews = {
title: string
description: string
items: TNewsResponse[]
}
export const CategorySection = (properties: TNews) => { export const CategorySection = (properties: TNews) => {
const { setIsSuccessOpen } = useNewsContext() const { setIsSuccessOpen } = useNewsContext()
const loaderData = useRouteLoaderData<typeof loader>('routes/_news') const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
const userData = loaderData?.userData const { userData } = loaderData || {}
const { title, description, items } = properties 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"> <div className="grid sm:grid-cols-3 sm:gap-x-8">
{items.map( {items.map(
({ featured, title, content, tags, slug, isPremium }, index) => ( (
{ featured_image, title, content, tags, slug, is_premium },
index,
) => (
<div <div
key={index} key={index}
className={twMerge('grid sm:gap-x-8')} className={twMerge('grid gap-3 sm:gap-x-8')}
> >
<img <img
className={twMerge( className={twMerge(
'aspect-[174/100] w-full rounded-md object-cover sm:aspect-[5/4]', 'aspect-[174/100] w-full rounded-md object-cover sm:aspect-[5/4]',
)} )}
src={featured} src={featured_image}
alt={title} alt={title}
/> />
<div className={twMerge('flex flex-col justify-between gap-4')}> <div className={twMerge('flex flex-col justify-between gap-4')}>
<div <Tags
className={twMerge( tags={tags}
'my-3 flex gap-2 uppercase max-sm:text-sm', is_premium={is_premium}
)} />
>
{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>
<div> <div>
<h3 <h3
@ -80,14 +77,14 @@ export const CategorySection = (properties: TNews) => {
> >
{title} {title}
</h3> </h3>
<p className="text-md mt-5 text-[#777777] sm:text-xl"> <p className="text-md mt-5 line-clamp-3 text-[#777777] sm:text-xl">
{content} {stripHtml(content).result}
</p> </p>
</div> </div>
<Button <Button
size="block" size="block"
{...getPremiumAttribute({ {...getPremiumAttribute({
isPremium, isPremium: is_premium,
slug, slug,
onClick: () => setIsSuccessOpen('warning'), onClick: () => setIsSuccessOpen('warning'),
userData, userData,

View File

@ -53,7 +53,7 @@ export const Combobox = <TFormValues extends Record<string, unknown>>(
className, className,
labelClassName, labelClassName,
containerClassName, containerClassName,
...rest ...restProperties
} = properties } = properties
const { const {
control, control,
@ -88,7 +88,7 @@ export const Combobox = <TFormValues extends Record<string, unknown>>(
onChange={field.onChange} onChange={field.onChange}
disabled={disabled} disabled={disabled}
immediate immediate
{...rest} {...restProperties}
> >
<div className="relative"> <div className="relative">
<ComboboxInput <ComboboxInput

View File

@ -40,7 +40,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
className, className,
containerClassName, containerClassName,
labelClassName, labelClassName,
...rest ...restProperties
} = properties } = properties
const [inputType, setInputType] = useState(type) const [inputType, setInputType] = useState(type)
@ -68,7 +68,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
)} )}
placeholder={inputType === 'password' ? '******' : placeholder} placeholder={inputType === 'password' ? '******' : placeholder}
{...register(name, rules)} {...register(name, rules)}
{...rest} {...restProperties}
/> />
{type === 'password' && ( {type === 'password' && (
<Button <Button

View File

@ -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 { ProfileIcon } from '~/components/icons/profile'
import { formatDate } from '~/utils/formatter' import { formatDate } from '~/utils/formatter'

View File

@ -35,37 +35,44 @@ export const SocialShareButtons = ({
return ( return (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{showPopup && ( <button
<div className="absolute top-0 rounded-lg border-2 border-gray-400 bg-white p-2 shadow-lg"> onClick={handleCopyLink}
Link berhasil disalin! className="relative cursor-pointer"
</div> >
)} <LinkIcon className="h-8 w-8 rounded-full bg-[#F4F4F4] p-2 sm:h-10 sm:w-10" />
<button onClick={handleCopyLink}> {showPopup && (
<LinkIcon className="h-8 w-8 rounded-full bg-gray-400 p-2 sm:h-10 sm:w-10" /> <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> </button>
<FacebookShareButton <FacebookShareButton
url={url} url={url}
title={title} 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> </FacebookShareButton>
<LinkedinShareButton <LinkedinShareButton
url={url} url={url}
title={title} 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> </LinkedinShareButton>
<TwitterShareButton <TwitterShareButton
url={url} url={url}
title={title} 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> </TwitterShareButton>
<button onClick={handleInstagramShare}> <button
<InstagramIcon className="h-8 w-8 rounded-full bg-gray-400 p-2 sm:h-10 sm:w-10" /> onClick={handleInstagramShare}
className="cursor-pointer"
>
<InstagramIcon className="h-8 w-8 rounded-full bg-[#F4F4F4] p-2 sm:h-10 sm:w-10" />
</button> </button>
</div> </div>
) )

View 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>
)
}

View File

@ -1,4 +1,3 @@
import type { TNews } from '~/types/news'
type TBanner = { type TBanner = {
id: number id: number
urlImage: string urlImage: string
@ -8,93 +7,6 @@ type TBanner = {
createdAt?: string 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[] = [ export const BANNER: TBanner[] = [
{ {
id: 1, id: 1,

View File

@ -8,7 +8,7 @@ import type { loader } from '~/routes/_admin.lg-admin'
export const Navbar = () => { export const Navbar = () => {
const loaderData = useRouteLoaderData<typeof loader>('routes/_admin.lg-admin') const loaderData = useRouteLoaderData<typeof loader>('routes/_admin.lg-admin')
const staffData = loaderData?.staffData const { staffData } = loaderData || {}
const fetcher = useFetcher() const fetcher = useFetcher()
return ( return (
@ -25,7 +25,7 @@ export const Navbar = () => {
</Link> </Link>
<div className="flex items-center gap-x-8"> <div className="flex items-center gap-x-8">
<Popover className="relative"> <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"> <div className="flex items-center space-x-3">
{staffData?.profile_picture ? ( {staffData?.profile_picture ? (
<img <img
@ -37,7 +37,7 @@ export const Navbar = () => {
<ProfileIcon className="h-8 w-8 rounded-full bg-[#C4C4C4]" /> <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> </div>
<ChevronIcon className="opacity-50" /> <ChevronIcon className="opacity-50" />
</PopoverButton> </PopoverButton>

View File

@ -43,7 +43,7 @@ export const FormRegister = () => {
const [disabled, setDisabled] = useState(false) const [disabled, setDisabled] = useState(false)
const fetcher = useFetcher() const fetcher = useFetcher()
const loaderData = useRouteLoaderData<typeof loader>('routes/_news') const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
const subscriptions = loaderData?.subscriptionsData const { subscriptionsData: subscriptions } = loaderData || {}
const formMethods = useRemixForm<TRegisterSchema>({ const formMethods = useRemixForm<TRegisterSchema>({
mode: 'onSubmit', mode: 'onSubmit',

View File

@ -31,7 +31,7 @@ export default function FormSubscription() {
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const [disabled, setDisabled] = useState(false) const [disabled, setDisabled] = useState(false)
const loaderData = useRouteLoaderData<typeof loader>('routes/_news') const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
const subscriptions = loaderData?.subscriptionsData const { subscriptionsData: subscriptions } = loaderData || {}
const formMethods = useRemixForm<TSubscribeSchema>({ const formMethods = useRemixForm<TSubscribeSchema>({
mode: 'onSubmit', mode: 'onSubmit',

View File

@ -18,7 +18,7 @@ export default function HeaderMenuMobile(properties: THeaderMenuMobile) {
const [isMenuOpen, setIsMenuOpen] = useState(false) const [isMenuOpen, setIsMenuOpen] = useState(false)
const { setIsLoginOpen } = useNewsContext() const { setIsLoginOpen } = useNewsContext()
const loaderData = useRouteLoaderData<typeof loader>('routes/_news') const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
const userData = loaderData?.userData const { userData } = loaderData || {}
const fetcher = useFetcher() const fetcher = useFetcher()
const handleToggleMenu = (): void => { const handleToggleMenu = (): void => {

View File

@ -8,7 +8,7 @@ import type { loader } from '~/routes/_news'
export const HeaderTop = () => { export const HeaderTop = () => {
const { setIsLoginOpen } = useNewsContext() const { setIsLoginOpen } = useNewsContext()
const loaderData = useRouteLoaderData<typeof loader>('routes/_news') const loaderData = useRouteLoaderData<typeof loader>('routes/_news')
const userData = loaderData?.userData const { userData } = loaderData || {}
const fetcher = useFetcher() const fetcher = useFetcher()
return ( return (

View File

@ -6,15 +6,14 @@ import type { TCategoryResponse } from '~/apis/common/get-categories'
import { Button } from '~/components/ui/button' import { Button } from '~/components/ui/button'
import { UiTable } from '~/components/ui/table' import { UiTable } from '~/components/ui/table'
import { TitleDashboard } from '~/components/ui/title-dashboard' 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 = () => { export const CategoriesPage = () => {
const loaderData = useRouteLoaderData<typeof loader>( const loaderData = useRouteLoaderData<typeof loader>(
'routes/_admin.lg-admin._dashboard.categories._index', 'routes/_admin.lg-admin._dashboard',
) )
const categoriesData = loaderData?.dataCategories
DataTable.use(DT) DataTable.use(DT)
const dataTable = categoriesData?.sort((a, b) => { const dataTable = loaderData?.categoriesData?.sort((a, b) => {
if (a.sequence === null) return 1 if (a.sequence === null) return 1
if (b.sequence === null) return -1 if (b.sequence === null) return -1
return a.sequence - b.sequence return a.sequence - b.sequence

View File

@ -2,8 +2,8 @@ import DT from 'datatables.net-dt'
import DataTable from 'datatables.net-react' import DataTable from 'datatables.net-react'
import { Link, useRouteLoaderData } from 'react-router' import { Link, useRouteLoaderData } from 'react-router'
import type { TNewsResponse } from '~/apis/admin/get-news'
import type { TCategoryResponse } from '~/apis/common/get-categories' 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 type { TTagResponse } from '~/apis/common/get-tags'
import { Button } from '~/components/ui/button' import { Button } from '~/components/ui/button'
import { UiTable } from '~/components/ui/table' import { UiTable } from '~/components/ui/table'
@ -15,10 +15,9 @@ export const ContentsPage = () => {
const loaderData = useRouteLoaderData<typeof loader>( const loaderData = useRouteLoaderData<typeof loader>(
'routes/_admin.lg-admin._dashboard.contents._index', 'routes/_admin.lg-admin._dashboard.contents._index',
) )
const newsData = loaderData?.newsData
DataTable.use(DT) 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(), (a, b) => new Date(b.live_at).getTime() - new Date(a.live_at).getTime(),
) )
const dataColumns = [ const dataColumns = [
@ -63,9 +62,12 @@ export const ContentsPage = () => {
<div className="text-sm text-[#7C7C7C]">ID: {data.id.slice(0, 8)}</div> <div className="text-sm text-[#7C7C7C]">ID: {data.id.slice(0, 8)}</div>
</div> </div>
), ),
4: (value: TCategoryResponse[]) => 4: (value: TCategoryResponse[]) => (
value.map((item) => item.name).join(', '), <div className="text-xs">{value.map((item) => item.name).join(', ')}</div>
5: (value: TTagResponse[]) => value.map((item) => item.name).join(', '), ),
5: (value: TTagResponse[]) => (
<div className="text-xs">{value.map((item) => item.name).join(', ')}</div>
),
6: (value: string) => 6: (value: string) =>
value ? ( value ? (
<div className="rounded-full bg-[#FFFCAF] px-2 text-center text-[#DBCA6E]"> <div className="rounded-full bg-[#FFFCAF] px-2 text-center text-[#DBCA6E]">

View File

@ -5,14 +5,13 @@ import { Link, useRouteLoaderData } from 'react-router'
import { Button } from '~/components/ui/button' import { Button } from '~/components/ui/button'
import { UiTable } from '~/components/ui/table' import { UiTable } from '~/components/ui/table'
import { TitleDashboard } from '~/components/ui/title-dashboard' 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 = () => { export const TagsPage = () => {
const loaderData = useRouteLoaderData<typeof loader>( const loaderData = useRouteLoaderData<typeof loader>(
'routes/_admin.lg-admin._dashboard.tags._index', 'routes/_admin.lg-admin._dashboard',
) )
const tagsData = loaderData?.tagsData const { tagsData: dataTable } = loaderData || {}
const dataTable = tagsData
DataTable.use(DT) DataTable.use(DT)
const dataColumns = [ const dataColumns = [

View File

@ -5,14 +5,14 @@ import { useFetcher, useNavigate, useRouteLoaderData } from 'react-router'
import { RemixFormProvider, useRemixForm } from 'remix-hook-form' import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
import { z } from 'zod' 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 { TextEditor } from '~/components/text-editor'
import { Button } from '~/components/ui/button' import { Button } from '~/components/ui/button'
import { Combobox } from '~/components/ui/combobox' import { Combobox } from '~/components/ui/combobox'
import { Input } from '~/components/ui/input' import { Input } from '~/components/ui/input'
import { Switch } from '~/components/ui/switch' import { Switch } from '~/components/ui/switch'
import { TitleDashboard } from '~/components/ui/title-dashboard' 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({ export const contentSchema = z.object({
id: z.string().optional(), id: z.string().optional(),
@ -48,7 +48,9 @@ export const contentSchema = z.object({
content: z.string().min(1, { content: z.string().min(1, {
message: 'Konten is required', 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(), is_premium: z.boolean().optional(),
live_at: z.string().min(1, { live_at: z.string().min(1, {
message: 'Tanggal live is required', message: 'Tanggal live is required',
@ -64,9 +66,11 @@ export const FormContentsPage = (properties: TProperties) => {
const { newsData } = properties || {} const { newsData } = properties || {}
const fetcher = useFetcher() const fetcher = useFetcher()
const navigate = useNavigate() const navigate = useNavigate()
const loaderData = useRouteLoaderData<typeof loader>('routes/_admin.lg-admin') const loaderData = useRouteLoaderData<typeof loader>(
const categories = loaderData?.categoriesData 'routes/_admin.lg-admin._dashboard',
const tags = loaderData?.tagsData )
const { categoriesData: categories } = loaderData || {}
const { tagsData: tags } = loaderData || {}
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const [disabled, setDisabled] = useState(false) const [disabled, setDisabled] = useState(false)

View File

@ -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',
},
],
}

View File

@ -1,18 +1,15 @@
import { useParams, useRouteLoaderData } from 'react-router' import { useRouteLoaderData } from 'react-router'
import { Card } from '~/components/ui/card' import { Card } from '~/components/ui/card'
import { CategorySection } from '~/components/ui/category-section' import { CategorySection } from '~/components/ui/category-section'
import type { loader } from '~/routes/_news' import type { loader } from '~/routes/_news.category.$code'
import { BERITA } from './data'
export const NewsCategoriesPage = () => { export const NewsCategoriesPage = () => {
const parameters = useParams() const loaderData = useRouteLoaderData<typeof loader>(
const loaderData = useRouteLoaderData<typeof loader>('routes/_news') 'routes/_news.category.$code',
const { name, description } = )
loaderData?.categoriesData.find((item) => item.code === parameters.code) || const { categoryData, newsData } = loaderData || {}
{} const { name, description } = categoryData || {}
const { items } = BERITA
return ( return (
<div className="relative"> <div className="relative">
@ -20,7 +17,7 @@ export const NewsCategoriesPage = () => {
<CategorySection <CategorySection
title={name || ''} title={name || ''}
description={description || ''} description={description || ''}
items={items} items={newsData || []}
/> />
</Card> </Card>
</div> </div>

View File

@ -2,22 +2,26 @@ import htmlParse from 'html-react-parser'
import { useReadingTime } from 'react-hook-reading-time' import { useReadingTime } from 'react-hook-reading-time'
import { useRouteLoaderData } from 'react-router' import { useRouteLoaderData } from 'react-router'
import type { TTagResponse } from '~/apis/common/get-tags'
import { Card } from '~/components/ui/card' import { Card } from '~/components/ui/card'
import { CarouselSection } from '~/components/ui/carousel-section' import { CarouselSection } from '~/components/ui/carousel-section'
import { NewsAuthor } from '~/components/ui/news-author' import { NewsAuthor } from '~/components/ui/news-author'
import { SocialShareButtons } from '~/components/ui/social-share' 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 { loader } from '~/routes/_news.detail.$slug'
import type { TNews } from '~/types/news'
export const NewsDetailPage = () => { export const NewsDetailPage = () => {
const loaderData = useRouteLoaderData<typeof loader>( const loaderData = useRouteLoaderData<typeof loader>(
'routes/_news.detail.$slug', 'routes/_news.detail.$slug',
) )
const berita: TNews = {
title: loaderData?.beritaCategory?.name || '',
description: loaderData?.beritaCategory?.description || '',
items: loaderData?.beritaNews || [],
}
const currentUrl = globalThis.location const currentUrl = globalThis.location
const { newsDetailData } = loaderData || {}
const { title, content, featured_image, author, live_at, tags } = const { title, content, featured_image, author, live_at, tags } =
newsDetailData || {} loaderData?.newsDetailData || {}
const { text } = useReadingTime(content || '') const { text } = useReadingTime(content || '')
@ -50,7 +54,7 @@ export const NewsDetailPage = () => {
</div> </div>
<div className="mt-8 flex items-center justify-center"> <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)} {content && htmlParse(content)}
</article> </article>
</div> </div>
@ -63,22 +67,14 @@ export const NewsDetailPage = () => {
title={`${title}`} title={`${title}`}
/> />
</div> </div>
<div className="flex flex-wrap items-end gap-2">
{tags?.map((tag: TTagResponse) => ( <Tags tags={tags || []} />
<span
key={tag.id}
className="rounded bg-gray-300 p-1"
>
{tag.name}
</span>
))}
</div>
</div> </div>
</div> </div>
</Card> </Card>
<Card className="bg-white p-5 max-sm:hidden"> <Card className="bg-white p-5 max-sm:hidden">
<CarouselSection {...BERITA} /> <CarouselSection {...berita} />
</Card> </Card>
</div> </div>
) )

View File

@ -1,25 +1,45 @@
import { useRouteLoaderData } from 'react-router'
import { Card } from '~/components/ui/card' import { Card } from '~/components/ui/card'
import { CarouselHero } from '~/components/ui/carousel-hero' import { CarouselHero } from '~/components/ui/carousel-hero'
import { CarouselSection } from '~/components/ui/carousel-section' import { CarouselSection } from '~/components/ui/carousel-section'
import { Newsletter } from '~/components/ui/newsletter' 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 = () => { 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 ( return (
<div className="relative"> <div className="relative">
<Card> <Card>
<CarouselHero {...SPOTLIGHT} /> <CarouselHero {...spotlight} />
</Card> </Card>
<div className="min-h-[400px] sm:min-h-[300px]"> <div className="min-h-[400px] sm:min-h-[300px]">
<Newsletter className="mr-0 sm:-ml-14" /> <Newsletter className="mr-0 sm:-ml-14" />
</div> </div>
<Card> <Card>
<CarouselSection {...BERITA} /> <CarouselSection {...berita} />
</Card>
<Card>
<CarouselSection {...kajian} />
</Card> </Card>
{/* <Card>
<CarouselSection {...KAJIAN} />
</Card> */}
</div> </div>
) )
} }

View File

@ -1,16 +1,4 @@
import { getCategories } from '~/apis/common/get-categories'
import { handleCookie } from '~/libs/cookies'
import { CategoriesPage } from '~/pages/dashboard-categories' 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 /> const DashboardCategoriesIndexLayout = () => <CategoriesPage />
export default DashboardCategoriesIndexLayout export default DashboardCategoriesIndexLayout

View File

@ -1,14 +1,10 @@
import { getCategories } from '~/apis/common/get-categories' import { getCategories } from '~/apis/common/get-categories'
import { handleCookie } from '~/libs/cookies'
import { FormCategoryPage } from '~/pages/form-category' import { FormCategoryPage } from '~/pages/form-category'
import type { Route } from './+types/_admin.lg-admin._dashboard.categories.update.$id' import type { Route } from './+types/_admin.lg-admin._dashboard.categories.update.$id'
export const loader = async ({ request, params }: Route.LoaderArgs) => { export const loader = async ({ params }: Route.LoaderArgs) => {
const { staffToken } = await handleCookie(request) const { data: categoriesData } = await getCategories()
const { data: categoriesData } = await getCategories({
accessToken: staffToken,
})
const categoryData = categoriesData.find( const categoryData = categoriesData.find(
(category) => category.id === params.id, (category) => category.id === params.id,
) )
@ -18,7 +14,7 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
const DashboardCategoriesUpdateLayout = ({ const DashboardCategoriesUpdateLayout = ({
loaderData, loaderData,
}: Route.ComponentProps) => { }: Route.ComponentProps) => {
const categoryData = loaderData.categoryData const { categoryData } = loaderData || {}
return <FormCategoryPage categoryData={categoryData} /> return <FormCategoryPage categoryData={categoryData} />
} }
export default DashboardCategoriesUpdateLayout export default DashboardCategoriesUpdateLayout

View File

@ -1,14 +1,10 @@
import { getNews } from '~/apis/admin/get-news' import { getNews } from '~/apis/common/get-news'
import { handleCookie } from '~/libs/cookies'
import { ContentsPage } from '~/pages/dashboard-contents' import { ContentsPage } from '~/pages/dashboard-contents'
import type { Route } from './+types/_admin.lg-admin._dashboard.contents._index' import type { Route } from './+types/_admin.lg-admin._dashboard.contents._index'
export const loader = async ({ request }: Route.LoaderArgs) => { export const loader = async ({}: Route.LoaderArgs) => {
const { staffToken } = await handleCookie(request) const { data: newsData } = await getNews()
const { data: newsData } = await getNews({
accessToken: staffToken,
})
return { newsData } return { newsData }
} }

View File

@ -14,7 +14,7 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
} }
const DashboardContentUpdateLayout = ({ loaderData }: Route.ComponentProps) => { const DashboardContentUpdateLayout = ({ loaderData }: Route.ComponentProps) => {
const newsData = loaderData.newsData const { newsData } = loaderData || {}
return <FormContentsPage newsData={newsData} /> return <FormContentsPage newsData={newsData} />
} }
export default DashboardContentUpdateLayout export default DashboardContentUpdateLayout

View File

@ -1,15 +1,4 @@
import { getTags } from '~/apis/common/get-tags'
import { handleCookie } from '~/libs/cookies'
import { TagsPage } from '~/pages/dashboard-tags' 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 /> const DashboardTagsIndexLayout = () => <TagsPage />
export default DashboardTagsIndexLayout export default DashboardTagsIndexLayout

View File

@ -1,20 +1,16 @@
import { getTags } from '~/apis/common/get-tags' import { getTags } from '~/apis/common/get-tags'
import { handleCookie } from '~/libs/cookies'
import { FormTagPage } from '~/pages/form-tag' import { FormTagPage } from '~/pages/form-tag'
import type { Route } from './+types/_admin.lg-admin._dashboard.tags.update.$id' import type { Route } from './+types/_admin.lg-admin._dashboard.tags.update.$id'
export const loader = async ({ request, params }: Route.LoaderArgs) => { export const loader = async ({ params }: Route.LoaderArgs) => {
const { staffToken } = await handleCookie(request) const { data: tagsData } = await getTags()
const { data: tagsData } = await getTags({
accessToken: staffToken,
})
const tagData = tagsData.find((tag) => tag.id === params.id) const tagData = tagsData.find((tag) => tag.id === params.id)
return { tagData } return { tagData }
} }
const DashboardTagUpdateLayout = ({ loaderData }: Route.ComponentProps) => { const DashboardTagUpdateLayout = ({ loaderData }: Route.ComponentProps) => {
const tagData = loaderData.tagData const { tagData } = loaderData || {}
return <FormTagPage tagData={tagData} /> return <FormTagPage tagData={tagData} />
} }
export default DashboardTagUpdateLayout export default DashboardTagUpdateLayout

View File

@ -1,7 +1,21 @@
import { Outlet } from 'react-router' 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 { 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 = () => { const DashboardLayout = () => {
return ( return (
<AdminDashboardLayout> <AdminDashboardLayout>

View File

@ -1,8 +1,6 @@
import { Outlet, redirect } from 'react-router' import { Outlet, redirect } from 'react-router'
import { getStaff } from '~/apis/admin/get-staff' 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 { AUTH_PAGES } from '~/configs/pages'
import { AdminDefaultLayout } from '~/layouts/admin/default' import { AdminDefaultLayout } from '~/layouts/admin/default'
import { handleCookie } from '~/libs/cookies' import { handleCookie } from '~/libs/cookies'
@ -29,13 +27,9 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
}) })
staffData = data staffData = data
} }
const { data: categoriesData } = await getCategories()
const { data: tagsData } = await getTags()
return { return {
staffData, staffData,
categoriesData,
tagsData,
} }
} }

View File

@ -1,5 +1,40 @@
import { getCategories } from '~/apis/common/get-categories'
import { getNews } from '~/apis/common/get-news'
import { NewsPage } from '~/pages/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 /> const NewsIndexLayout = () => <NewsPage />
export default NewsIndexLayout export default NewsIndexLayout

View File

@ -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 { 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 /> const NewsCategoriesLayout = () => <NewsCategoriesPage />
export default NewsCategoriesLayout export default NewsCategoriesLayout

View File

@ -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 { getNewsBySlug } from '~/apis/common/get-news-by-slug'
import { getUser } from '~/apis/news/get-user'
import { APP } from '~/configs/meta' import { APP } from '~/configs/meta'
import { handleCookie } from '~/libs/cookies' import { handleCookie } from '~/libs/cookies'
import { NewsDetailPage } from '~/pages/news-detail' 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) => { export const loader = async ({ request, params }: Route.LoaderArgs) => {
const { userToken } = await handleCookie(request) 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({ const { data: newsDetailData } = await getNewsBySlug({
slug: params.slug, slug: params.slug,
accessToken: userToken, 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 { return {
newsDetailData, newsDetailData,
userData, beritaCategory,
beritaNews,
} }
} }

View File

@ -1,20 +1,7 @@
import type { TNewsResponse } from '~/apis/common/get-news'
export type TNews = { export type TNews = {
title: string title: string
description: string description: string
items: Pick< items: TNewsResponse[]
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>
} }

View File

@ -33,6 +33,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"datatables.net-dt": "^2.2.2", "datatables.net-dt": "^2.2.2",
"datatables.net-react": "^1.0.0", "datatables.net-react": "^1.0.0",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^8.5.2", "embla-carousel-react": "^8.5.2",
"html-react-parser": "^5.2.2", "html-react-parser": "^5.2.2",
"isbot": "^5.1.17", "isbot": "^5.1.17",
@ -46,6 +47,7 @@
"react-router": "^7.1.3", "react-router": "^7.1.3",
"react-share": "^5.2.2", "react-share": "^5.2.2",
"remix-hook-form": "^6.1.3", "remix-hook-form": "^6.1.3",
"string-strip-html": "^13.4.12",
"tailwind-merge": "^3.0.1", "tailwind-merge": "^3.0.1",
"xior": "^0.6.3", "xior": "^0.6.3",
"zod": "^3.24.2" "zod": "^3.24.2"

115
pnpm-lock.yaml generated
View File

@ -65,6 +65,9 @@ importers:
datatables.net-react: datatables.net-react:
specifier: ^1.0.0 specifier: ^1.0.0
version: 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: embla-carousel-react:
specifier: ^8.5.2 specifier: ^8.5.2
version: 8.5.2(react@19.0.0) version: 8.5.2(react@19.0.0)
@ -104,6 +107,9 @@ importers:
remix-hook-form: remix-hook-form:
specifier: ^6.1.3 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) 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: tailwind-merge:
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.1 version: 3.0.1
@ -1653,6 +1659,9 @@ packages:
'@types/linkify-it@5.0.0': '@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.16': '@types/lodash@4.17.16':
resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==} resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==}
@ -1993,6 +2002,10 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
codsen-utils@1.6.7:
resolution: {integrity: sha512-M+9D3IhFAk4T8iATX62herVuIx1sp5kskWgxEegKD/JwTTSSGjGQs5Q5J4vVJ4mLcn1uhfxDYv6Yzr8zleHF3w==}
engines: {node: '>=14.18.0'}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@ -2321,6 +2334,11 @@ packages:
electron-to-chromium@1.5.90: electron-to-chromium@1.5.90:
resolution: {integrity: sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==} 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: embla-carousel-react@8.5.2:
resolution: {integrity: sha512-Tmx+uY3MqseIGdwp0ScyUuxpBgx5jX1f7od4Cm5mDwg/dptEiTKf9xp6tw0lZN2VA9JbnVMl/aikmbc53c6QFA==} resolution: {integrity: sha512-Tmx+uY3MqseIGdwp0ScyUuxpBgx5jX1f7od4Cm5mDwg/dptEiTKf9xp6tw0lZN2VA9JbnVMl/aikmbc53c6QFA==}
peerDependencies: peerDependencies:
@ -2827,6 +2845,9 @@ packages:
html-dom-parser@5.0.13: html-dom-parser@5.0.13:
resolution: {integrity: sha512-B7JonBuAfG32I7fDouUQEogBrz3jK9gAuN1r1AaXpED6dIhtg/JwiSRhjGL7aOJwRz3HU4efowCjQBaoXiREqg==} resolution: {integrity: sha512-B7JonBuAfG32I7fDouUQEogBrz3jK9gAuN1r1AaXpED6dIhtg/JwiSRhjGL7aOJwRz3HU4efowCjQBaoXiREqg==}
html-entities@2.5.2:
resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==}
html-react-parser@5.2.2: html-react-parser@5.2.2:
resolution: {integrity: sha512-yA5012CJGSFWYZsgYzfr6HXJgDap38/AEP4ra8Cw+WHIi2ZRDXRX/QVYdumRf1P8zKyScKd6YOrWYvVEiPfGKg==} resolution: {integrity: sha512-yA5012CJGSFWYZsgYzfr6HXJgDap38/AEP4ra8Cw+WHIi2ZRDXRX/QVYdumRf1P8zKyScKd6YOrWYvVEiPfGKg==}
peerDependencies: peerDependencies:
@ -3272,6 +3293,9 @@ packages:
resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 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: lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
@ -3857,6 +3881,22 @@ packages:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'} 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: raw-body@2.5.2:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -4246,6 +4286,22 @@ packages:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'} 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: string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -4358,6 +4414,9 @@ packages:
through@2.3.8: through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} 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: tiny-lru@11.2.11:
resolution: {integrity: sha512-27BIW0dIWTYYoWNnqSmoNMKe5WIbkXsc0xaCQHd3/3xT2XMuMJrzHdrO9QBFR14emBz1Bu0dOAs2sCBBrvgPQA==} resolution: {integrity: sha512-27BIW0dIWTYYoWNnqSmoNMKe5WIbkXsc0xaCQHd3/3xT2XMuMJrzHdrO9QBFR14emBz1Bu0dOAs2sCBBrvgPQA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -6216,6 +6275,10 @@ snapshots:
'@types/linkify-it@5.0.0': {} '@types/linkify-it@5.0.0': {}
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.16
'@types/lodash@4.17.16': {} '@types/lodash@4.17.16': {}
'@types/markdown-it@14.1.2': '@types/markdown-it@14.1.2':
@ -6615,6 +6678,10 @@ snapshots:
clsx@2.1.1: {} clsx@2.1.1: {}
codsen-utils@1.6.7:
dependencies:
rfdc: 1.4.1
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
@ -6931,6 +6998,10 @@ snapshots:
electron-to-chromium@1.5.90: {} 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): embla-carousel-react@8.5.2(react@19.0.0):
dependencies: dependencies:
embla-carousel: 8.5.2 embla-carousel: 8.5.2
@ -7632,6 +7703,8 @@ snapshots:
domhandler: 5.0.3 domhandler: 5.0.3
htmlparser2: 10.0.0 htmlparser2: 10.0.0
html-entities@2.5.2: {}
html-react-parser@5.2.2(@types/react@19.0.8)(react@19.0.0): html-react-parser@5.2.2(@types/react@19.0.8)(react@19.0.0):
dependencies: dependencies:
domhandler: 5.0.3 domhandler: 5.0.3
@ -8061,6 +8134,8 @@ snapshots:
dependencies: dependencies:
p-locate: 6.0.0 p-locate: 6.0.0
lodash-es@4.17.21: {}
lodash.camelcase@4.3.0: {} lodash.camelcase@4.3.0: {}
lodash.castarray@4.4.0: {} lodash.castarray@4.4.0: {}
@ -8586,6 +8661,25 @@ snapshots:
range-parser@1.2.1: {} 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: raw-body@2.5.2:
dependencies: dependencies:
bytes: 3.1.2 bytes: 3.1.2
@ -9045,6 +9139,25 @@ snapshots:
string-argv@0.3.2: {} 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: string-width@4.2.3:
dependencies: dependencies:
emoji-regex: 8.0.0 emoji-regex: 8.0.0
@ -9174,6 +9287,8 @@ snapshots:
through@2.3.8: {} through@2.3.8: {}
tiny-invariant@1.3.3: {}
tiny-lru@11.2.11: {} tiny-lru@11.2.11: {}
tinyexec@0.3.2: {} tinyexec@0.3.2: {}