Compare commits
7 Commits
a635dc1431
...
661437a2d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
661437a2d3 | ||
|
|
529ac6b590 | ||
|
|
d3dc3b5016 | ||
|
|
b61235a3b5 | ||
|
|
4754e75f59 | ||
|
|
d5c77dc13b | ||
|
|
6e555184f0 |
@ -2,28 +2,31 @@ import { z } from 'zod'
|
|||||||
|
|
||||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||||
|
|
||||||
|
const authorSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
profile_picture: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const categoriesCodeSchema = z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
code: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
const newsSchema = z.object({
|
const newsSchema = z.object({
|
||||||
data: z.array(
|
data: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
categories: z.array(
|
categories: categoriesCodeSchema,
|
||||||
z.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
code: z.string(),
|
|
||||||
created_at: z.string(),
|
|
||||||
updated_at: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
tags: z.array(
|
tags: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
code: z.string(),
|
code: z.string(),
|
||||||
created_at: z.string(),
|
|
||||||
updated_at: z.string(),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
is_premium: z.boolean(),
|
is_premium: z.boolean(),
|
||||||
@ -33,10 +36,14 @@ const newsSchema = z.object({
|
|||||||
live_at: z.string(),
|
live_at: z.string(),
|
||||||
created_at: z.string(),
|
created_at: z.string(),
|
||||||
updated_at: z.string(),
|
updated_at: z.string(),
|
||||||
|
author: authorSchema,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type TAuthor = z.infer<typeof authorSchema>
|
||||||
|
export type TCategories = z.infer<typeof categoriesCodeSchema>
|
||||||
|
|
||||||
export const getNews = async (parameters: THttpServer) => {
|
export const getNews = async (parameters: THttpServer) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await HttpServer(parameters).get(`/api/news`)
|
const { data } = await HttpServer(parameters).get(`/api/news`)
|
||||||
|
|||||||
@ -6,9 +6,20 @@ const userSchema = z.object({
|
|||||||
data: z.object({
|
data: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
subscribe_plan_code: z.string(),
|
phone: z.string(),
|
||||||
subscribe_plan_name: z.string(),
|
subscribe: z.object({
|
||||||
subscribe_status: z.string(),
|
id: z.string(),
|
||||||
|
subscribe_plan_id: z.string(),
|
||||||
|
start_date: z.string(),
|
||||||
|
end_date: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
auto_renew: z.boolean(),
|
||||||
|
subscribe_plan: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
code: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
55
app/app.css
55
app/app.css
@ -33,26 +33,43 @@ table.dataTable tbody > tr > td {
|
|||||||
border-bottom: 1px solid #ebebeb;
|
border-bottom: 1px solid #ebebeb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .embla.hero {
|
nav[aria-label='pagination'] {
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.embla__container.hero {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: yellow;
|
justify-content: center;
|
||||||
|
}
|
||||||
|
/* Style untuk tombol aktif (current) */
|
||||||
|
.dt-paging-button.current {
|
||||||
|
background-color: #2e2f7c !important;
|
||||||
|
color: white !important;
|
||||||
}
|
}
|
||||||
.embla__slide.hero {
|
|
||||||
flex: 0 0 100%;
|
|
||||||
min-width: 0;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .embla__slide {
|
/* Style tombol aktif, kecuali jika disabled */
|
||||||
margin-right: 10px;
|
div.dt-container .dt-paging .dt-paging-button.current:not(.disabled),
|
||||||
flex: 0 0 auto;
|
div.dt-container .dt-paging .dt-paging-button.current:not(.disabled):hover {
|
||||||
min-width: 0;
|
color: white !important;
|
||||||
max-width: 100%;
|
background-color: #2e2f7c !important;
|
||||||
|
min-width: 24px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style tombol disabled */
|
||||||
|
div.dt-container .dt-paging .dt-paging-button.disabled {
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: #ccc !important;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none; /* Agar tidak bisa diklik */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menghindari hover effect untuk tombol yang disabled */
|
||||||
|
div.dt-container .dt-paging .dt-paging-button:not(.disabled):hover {
|
||||||
|
background-color: #2e2f7c !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style default tombol */
|
||||||
|
div.dt-container .dt-paging .dt-paging-button {
|
||||||
|
min-width: 24px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: #2e2f7c !important;
|
||||||
}
|
}
|
||||||
@media (min-width: 768px) {
|
|
||||||
.embla__slide {
|
|
||||||
margin-right: 30px;
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
|
|||||||
@ -3,14 +3,17 @@ import DataTable from 'datatables.net-react'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export type UiTableProperties = {
|
export type UiTableProperties = {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
data: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
data: any[]
|
|
||||||
columns: ConfigColumns[]
|
columns: ConfigColumns[]
|
||||||
slots?: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
slots?: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
options?: Config
|
options?: Config
|
||||||
title: string
|
title: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderPaginationIcon = (icon: string) => {
|
||||||
|
return `<div class="pagination-icon">${icon}</div>`
|
||||||
|
}
|
||||||
|
|
||||||
export const UiTable: React.FC<UiTableProperties> = ({
|
export const UiTable: React.FC<UiTableProperties> = ({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
@ -33,6 +36,22 @@ export const UiTable: React.FC<UiTableProperties> = ({
|
|||||||
searching: true,
|
searching: true,
|
||||||
ordering: true,
|
ordering: true,
|
||||||
info: true,
|
info: true,
|
||||||
|
language: {
|
||||||
|
paginate: {
|
||||||
|
first: renderPaginationIcon(
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 "><path stroke-linecap="round" stroke-linejoin="round" d="m18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5" /></svg>`,
|
||||||
|
),
|
||||||
|
previous: renderPaginationIcon(
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" /></svg>`,
|
||||||
|
),
|
||||||
|
next: renderPaginationIcon(
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg>`,
|
||||||
|
),
|
||||||
|
last: renderPaginationIcon(
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4"> <path stroke-linecap="round" stroke-linejoin="round" d="m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5" /> </svg>`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -95,67 +95,6 @@ export const BERITA: TNews = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KAJIAN: TNews = {
|
|
||||||
title: 'KAJIAN',
|
|
||||||
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'],
|
|
||||||
isPremium: true,
|
|
||||||
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'],
|
|
||||||
isPremium: true,
|
|
||||||
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'],
|
|
||||||
isPremium: true,
|
|
||||||
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: 'https://placehold.co/600x400.png',
|
|
||||||
tags: ['Hukum Property'],
|
|
||||||
isPremium: true,
|
|
||||||
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 Property'],
|
|
||||||
isPremium: true,
|
|
||||||
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,
|
||||||
|
|||||||
@ -2,10 +2,12 @@ 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 { TCategories } from '~/apis/admin/get-news'
|
||||||
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.contents'
|
import type { loader } from '~/routes/_admin.lg-admin._dashboard.contents'
|
||||||
|
import { formatDate } from '~/utils/formatter'
|
||||||
|
|
||||||
export const ContentsPage = () => {
|
export const ContentsPage = () => {
|
||||||
const loaderData = useRouteLoaderData<typeof loader>(
|
const loaderData = useRouteLoaderData<typeof loader>(
|
||||||
@ -16,11 +18,22 @@ export const ContentsPage = () => {
|
|||||||
DataTable.use(DT)
|
DataTable.use(DT)
|
||||||
const dataTable = newsData
|
const dataTable = newsData
|
||||||
const dataColumns = [
|
const dataColumns = [
|
||||||
{ title: 'No', data: 'id' },
|
{
|
||||||
{ title: 'Tanggal Konten', data: 'created_at' },
|
title: 'No',
|
||||||
{ title: 'Nama Penulis', data: 'author_id' },
|
data: undefined,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
render: function (data: any, type: any, row: any, meta: any) {
|
||||||
|
return meta.row + 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: 'Tanggal Konten', data: 'live_at' },
|
||||||
|
{
|
||||||
|
title: 'Nama Penulis',
|
||||||
|
|
||||||
|
data: 'author',
|
||||||
|
},
|
||||||
{ title: 'Judul', data: 'title' },
|
{ title: 'Judul', data: 'title' },
|
||||||
// { title: 'Kategori', data: 'category' },
|
{ title: 'Kategori', data: 'categories' },
|
||||||
{
|
{
|
||||||
title: 'Tags',
|
title: 'Tags',
|
||||||
data: 'is_premium',
|
data: 'is_premium',
|
||||||
@ -30,12 +43,22 @@ export const ContentsPage = () => {
|
|||||||
: `<span class="bg-[#F5F5F5] text-[#4C5CA0] px-4 py-2 rounded-full">Normal</span>`
|
: `<span class="bg-[#F5F5F5] text-[#4C5CA0] px-4 py-2 rounded-full">Normal</span>`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// title: 'Action',
|
title: 'Action',
|
||||||
// data: 'id',
|
data: 'slug',
|
||||||
// },
|
},
|
||||||
]
|
]
|
||||||
const dataSlot = {
|
const dataSlot = {
|
||||||
|
1: (value: string) => {
|
||||||
|
return formatDate(value)
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
2: (value: any, type: any, data: any) =>
|
||||||
|
`<span>${value.name}</span> <br> <span class="text-sm text-[#7C7C7C]">ID: ${data.id.slice(0, 12)}</span>`,
|
||||||
|
4: (value: TCategories) => {
|
||||||
|
const categories = value.map((item) => item.name).join(', ')
|
||||||
|
return `${categories}`
|
||||||
|
},
|
||||||
6: (value: string | number) => {
|
6: (value: string | number) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { DUMMY_DESCRIPTION } from '~/data/contents'
|
import type { TNewsDetail } from '~/types/news'
|
||||||
import type { TNews, TNewsDetail } from '~/types/news'
|
|
||||||
|
|
||||||
export const CONTENT: TNewsDetail = {
|
export const CONTENT: TNewsDetail = {
|
||||||
title: 'Hotman Paris Membuka Perpustakaan di tengah Diskotik',
|
title: 'Hotman Paris Membuka Perpustakaan di tengah Diskotik',
|
||||||
@ -38,35 +37,3 @@ export const CONTENT: TNewsDetail = {
|
|||||||
categories: [],
|
categories: [],
|
||||||
tags: ['Category', 'Popular', 'Trending', 'Latest'],
|
tags: ['Category', 'Popular', 'Trending', 'Latest'],
|
||||||
}
|
}
|
||||||
|
|
||||||
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: '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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export const NewsDetailPage = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<IconsSocial className="flex-row-reverse" />
|
<IconsSocial className="flex-row" />
|
||||||
</div>
|
</div>
|
||||||
{/* end next planing create component for this section */}
|
{/* end next planing create component for this section */}
|
||||||
<div className="w-full bg-amber-200">
|
<div className="w-full bg-amber-200">
|
||||||
@ -50,7 +50,7 @@ export const NewsDetailPage = () => {
|
|||||||
{htmlParse(content)}
|
{htmlParse(content)}
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div className="items-end justify-between border-b-3 border-b-gray-300 py-4 sm:flex">
|
<div className="items-end justify-between border-b-gray-300 py-4 sm:flex">
|
||||||
<div className="flex flex-col max-sm:mb-3">
|
<div className="flex flex-col max-sm:mb-3">
|
||||||
<p className="mb-2">Share this post</p>
|
<p className="mb-2">Share this post</p>
|
||||||
<IconsSocial className="a" />
|
<IconsSocial className="a" />
|
||||||
@ -66,18 +66,6 @@ export const NewsDetailPage = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex items-center gap-2 align-middle">
|
|
||||||
<img
|
|
||||||
src={'https://placehold.co/50x50.png'}
|
|
||||||
alt={title}
|
|
||||||
className="h-12 w-12 rounded-full"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-md">{author}</h4>
|
|
||||||
<p className="text-sm">Job title, Company name</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,11 @@
|
|||||||
export const formatNumberWithPeriods = (number: number) => {
|
export const formatNumberWithPeriods = (number: number) => {
|
||||||
return new Intl.NumberFormat('id-ID').format(number)
|
return new Intl.NumberFormat('id-ID').format(number)
|
||||||
}
|
}
|
||||||
|
export const formatDate = (isoDate: string): string => {
|
||||||
|
const date = new Date(isoDate)
|
||||||
|
const day = date.getDate().toString().padStart(2, '0')
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0') // Month is zero-based
|
||||||
|
const year = date.getFullYear()
|
||||||
|
|
||||||
|
return `${day}/${month}/${year}`
|
||||||
|
}
|
||||||
|
|||||||
@ -12,7 +12,10 @@ type TGetPremiumAttribute = {
|
|||||||
|
|
||||||
export const getPremiumAttribute = (parameters: TGetPremiumAttribute) => {
|
export const getPremiumAttribute = (parameters: TGetPremiumAttribute) => {
|
||||||
const { isPremium, slug, onClick, userData } = parameters
|
const { isPremium, slug, onClick, userData } = parameters
|
||||||
if (isPremium && (!userData || userData?.subscribe_plan_code === 'basic')) {
|
if (
|
||||||
|
isPremium &&
|
||||||
|
(!userData || userData?.subscribe.subscribe_plan.code === 'basic')
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
onClick,
|
onClick,
|
||||||
to: '',
|
to: '',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user