This commit is contained in:
Anang Yusman 2025-10-06 23:55:07 +08:00
parent 7a7ef46284
commit 89eccf57c1
7 changed files with 223 additions and 44 deletions

29
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,29 @@
stages:
- build
- deploy
build-dev:
stage: build
when: on_success
only:
- main
image: docker:stable
services:
- name: docker:dind
command: ["--insecure-registry=103.82.242.92:8900"]
script:
- docker logout
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 103.82.242.92:8900
- docker build -t 103.82.242.92:8900/medols/web-bhayangkara-kita:dev .
- docker push 103.82.242.92:8900/medols/web-bhayangkara-kita:dev
auto-deploy:
stage: deploy
when: on_success
only:
- main
image: curlimages/curl:latest
services:
- docker:dind
script:
- curl --user admin:$JENKINS_PWD http://38.47.180.165:8080/job/auto-deploy-bhayangkara-kita/build?token=autodeploymedols

36
Dockerfile Normal file
View File

@ -0,0 +1,36 @@
# Menggunakan image Node.js yang lebih ringan
FROM node:23.5.0-alpine
# Mengatur port
ENV PORT 3000
# Install pnpm secara global
RUN npm install -g pnpm
# Membuat direktori aplikasi dan mengatur sebagai working directory
WORKDIR /usr/src/app
# Menyalin file penting terlebih dahulu untuk caching
COPY package.json ./
# Menyalin direktori ckeditor5 jika diperlukan
COPY vendor/ckeditor5 ./vendor/ckeditor5
# Menyalin env
COPY .env .env
# Install dependencies
RUN pnpm install
# RUN pnpm install --frozen-lockfile
# Menyalin source code aplikasi
COPY . .
# Build aplikasi
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm next build
# Expose port untuk server
EXPOSE 3000
# Perintah untuk menjalankan aplikasi
CMD ["pnpm", "run", "start"]

View File

@ -6,6 +6,7 @@ import { getArticleById, getListArticle } from "@/service/article";
import { close, loading } from "@/config/swal"; import { close, loading } from "@/config/swal";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { CommentIcon } from "../icons/sidebar-icon"; import { CommentIcon } from "../icons/sidebar-icon";
import { Link2, MailIcon } from "lucide-react";
type TabKey = "trending" | "comments" | "latest"; type TabKey = "trending" | "comments" | "latest";
@ -365,7 +366,47 @@ export default function DetailContent() {
}} }}
/> />
</div> </div>
<div className="w-full bg-white py-6">
<p className="mx-10 text-2xl mb-4 ">AUTHOR</p>
<div className=" border border-black p-6 flex items-center gap-6 max-w-[1200px] mx-auto">
{/* Foto Profil */}
<div className="w-20 h-20 relative ">
<Image
src="/profile.jpg"
alt="Author"
fill
className="rounded-full object-cover"
/>
</div>
{/* Info Author */}
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-800">
{articleDetail?.createdByName}
</h3>
<div className="mt-2 flex items-center gap-4 flex-wrap">
{/* Button lihat semua post */}
<button className="text-sm font-medium text-white hover:underline bg-[#655997] py-1 px-5 rounded-xl">
Lihat Semua Pos
</button>
<div className="bg-[#655997] rounded-full p-1">
<MailIcon
size={18}
className="text-white hover:text-black cursor-pointer "
></MailIcon>
</div>
<div className="bg-[#655997] rounded-full p-1">
<Link2
size={18}
className="text-white hover:text-black cursor-pointer "
></Link2>
</div>
</div>
</div>
</div>
</div>
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center">
<span className="font-semibold text-sm text-gray-700"> <span className="font-semibold text-sm text-gray-700">
Tags: Tags:

View File

@ -46,6 +46,7 @@ import {
TableCell, TableCell,
} from "@/components/ui/table"; } from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination"; import CustomPagination from "../layout/custom-pagination";
import DatePicker from "react-datepicker";
const columns = [ const columns = [
{ name: "No", uid: "no" }, { name: "No", uid: "no" },
@ -60,6 +61,7 @@ const columns = [
const columnsOtherRole = [ const columnsOtherRole = [
{ name: "No", uid: "no" }, { name: "No", uid: "no" },
{ name: "Judul", uid: "title" }, { name: "Judul", uid: "title" },
{ name: "Source", uid: "source" },
{ name: "Kategori", uid: "category" }, { name: "Kategori", uid: "category" },
{ name: "Tanggal Unggah", uid: "createdAt" }, { name: "Tanggal Unggah", uid: "createdAt" },
{ name: "Kreator", uid: "createdByName" }, { name: "Kreator", uid: "createdByName" },
@ -84,7 +86,9 @@ export default function ArticleTable() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [categories, setCategories] = useState<any>([]); const [categories, setCategories] = useState<any>([]);
const [selectedCategories, setSelectedCategories] = useState<any>(""); const [selectedCategories, setSelectedCategories] = useState<any>("");
const [startDateValue, setStartDateValue] = useState({ const [selectedSource, setSelectedSource] = useState<any>("");
const [selectedStatus, setSelectedStatus] = useState<string>("");
const [dateRange, setDateRange] = useState<any>({
startDate: null, startDate: null,
endDate: null, endDate: null,
}); });
@ -98,24 +102,49 @@ export default function ArticleTable() {
const res = await getArticleByCategory(); const res = await getArticleByCategory();
const data = res?.data?.data; const data = res?.data?.data;
setCategories(data); setCategories(data);
console.log("category", data);
} }
useEffect(() => {
initState();
}, [
page,
showData,
search,
selectedCategories,
selectedSource,
dateRange,
selectedStatus,
]);
async function initState() { async function initState() {
loading(); loading();
const req = { const req = {
limit: showData, limit: showData,
page: page, page: page,
search: search, search: search,
categorySlug: Array.from(selectedCategories).join(","), category: selectedCategories || "",
source: selectedSource || "",
isPublish:
selectedStatus !== "" ? selectedStatus === "publish" : undefined,
startDate: dateRange.startDate
? new Date(dateRange.startDate).toISOString()
: "",
endDate: dateRange.endDate
? new Date(dateRange.endDate).toISOString()
: "",
sort: "desc", sort: "desc",
sortBy: "created_at", sortBy: "created_at",
}; };
const res = await getArticlePagination(req); const res = await getArticlePagination(req);
await getTableNumber(parseInt(showData), res.data?.data);
let data = res.data?.data || [];
await getTableNumber(parseInt(showData), data);
setTotalPage(res?.data?.meta?.totalPage); setTotalPage(res?.data?.meta?.totalPage);
close(); close();
} }
// panggil ulang setiap state berubah // panggil ulang setiap state berubah
useEffect(() => { useEffect(() => {
initState(); initState();
@ -173,11 +202,11 @@ export default function ArticleTable() {
initState(); initState();
}; };
const copyUrlArticle = async (id: number, slug: string) => { const copyUrlArticle = async (id: number) => {
const url = const url =
`${window.location.protocol}//${window.location.host}` + `${window.location.protocol}//${window.location.host}` +
"/news/detail/" + "/detail/" +
`${id}-${slug}`; `${id}`;
try { try {
await navigator.clipboard.writeText(url); await navigator.clipboard.writeText(url);
successToast("Success", "Article Copy to Clipboard"); successToast("Success", "Article Copy to Clipboard");
@ -228,9 +257,7 @@ export default function ArticleTable() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56"> <DropdownMenuContent className="w-56">
<DropdownMenuItem <DropdownMenuItem onClick={() => copyUrlArticle(article.id)}>
onClick={() => copyUrlArticle(article.id, article.slug)}
>
<CopyIcon className="mr-2 h-4 w-4" /> <CopyIcon className="mr-2 h-4 w-4" />
Copy Url Article Copy Url Article
</DropdownMenuItem> </DropdownMenuItem>
@ -245,18 +272,15 @@ export default function ArticleTable() {
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
{(username === "admin-mabes" || <DropdownMenuItem asChild>
Number(userId) === article.createdById) && ( <Link
<DropdownMenuItem asChild> href={`/admin/article/edit/${article.id}`}
<Link className="flex items-center"
href={`/admin/article/edit/${article.id}`} >
className="flex items-center" <CreateIconIon className="mr-2 h-4 w-4" />
> Edit
<CreateIconIon className="mr-2 h-4 w-4" /> </Link>
Edit </DropdownMenuItem>
</Link>
</DropdownMenuItem>
)}
{username === "admin-mabes" && ( {username === "admin-mabes" && (
<DropdownMenuItem <DropdownMenuItem
@ -271,13 +295,10 @@ export default function ArticleTable() {
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{(username === "admin-mabes" || <DropdownMenuItem onClick={() => handleDelete(article.id)}>
Number(userId) === article.createdById) && ( <DeleteIcon className="mr-2 h-4 w-4 text-red-500" />
<DropdownMenuItem onClick={() => handleDelete(article.id)}> Delete
<DeleteIcon className="mr-2 h-4 w-4 text-red-500" /> </DropdownMenuItem>
Delete
</DropdownMenuItem>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
@ -354,24 +375,63 @@ export default function ArticleTable() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{categories {categories
?.filter((category: any) => category.slug != null) ?.filter((category: any) => category.title != null)
.map((category: any) => ( .map((category: any) => (
<SelectItem key={category.slug} value={category.slug}> <SelectItem key={category.id} value={category.title}>
{category.title} {category.title}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{/* <div className="flex flex-col gap-1 w-full lg:w-[240px]"> <div className="flex flex-col gap-1 w-full lg:w-[150px]">
<p className="font-semibold text-sm">Source</p>
<Select
value={selectedSource}
onValueChange={(value) => setSelectedSource(value)}
>
<SelectTrigger className="w-full text-sm border">
<SelectValue placeholder="Pilih Source" />
</SelectTrigger>
<SelectContent>
<SelectItem value="INTERNAL">INTERNAL</SelectItem>
<SelectItem value="EXTERNAL">EXTERNAL</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1 w-full lg:w-[150px]">
<p className="font-semibold text-sm">Status</p>
<Select
value={selectedStatus}
onValueChange={(value) => setSelectedStatus(value)}
>
<SelectTrigger className="w-full text-sm border">
<SelectValue placeholder="Pilih Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="publish">PUBLISH</SelectItem>
<SelectItem value="draft">DRAFT</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1 w-full lg:w-[240px]">
<p className="font-semibold text-sm">Tanggal</p> <p className="font-semibold text-sm">Tanggal</p>
<Datepicker <DatePicker
value={startDateValue} selectsRange
displayFormat="DD/MM/YYYY" startDate={dateRange.startDate}
onChange={(e: any) => setStartDateValue(e)} endDate={dateRange.endDate}
inputClassName="z-50 w-full text-sm bg-transparent border-1 border-gray-200 px-2 py-[6px] rounded-xl h-[40px] text-gray-600 dark:text-gray-300" onChange={(update: [Date | null, Date | null]) => {
setDateRange({
startDate: update[0],
endDate: update[1],
});
}}
isClearable
dateFormat="dd/MM/yyyy"
className="z-50 w-full text-sm bg-transparent border border-gray-200 px-2 py-[6px] rounded-xl h-[40px] text-gray-600 dark:text-gray-300"
placeholderText="Pilih rentang tanggal"
/> />
</div> */} </div>
</div> </div>
<div className="w-full overflow-x-hidden"> <div className="w-full overflow-x-hidden">
<div className="w-full mx-auto overflow-x-hidden"> <div className="w-full mx-auto overflow-x-hidden">

BIN
public/profile.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,6 +1,11 @@
import { PaginationRequest } from "@/types/globals"; import { PaginationRequest } from "@/types/globals";
import { httpGet } from "./http-config/http-base-services"; import { httpGet } from "./http-config/http-base-services";
import { httpDeleteInterceptor, httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services"; import {
httpDeleteInterceptor,
httpGetInterceptor,
httpPostInterceptor,
httpPutInterceptor,
} from "./http-config/http-interceptor-services";
export async function getListArticle(props: PaginationRequest) { export async function getListArticle(props: PaginationRequest) {
const { const {
@ -40,13 +45,20 @@ export async function getArticlePagination(props: PaginationRequest) {
sort, sort,
categorySlug, categorySlug,
isBanner, isBanner,
isPublish,
source,
} = props; } = props;
return await httpGetInterceptor( return await httpGetInterceptor(
`/articles?limit=${limit}&page=${page}&title=${search}&startDate=${startDate || ""}&endDate=${ `/articles?limit=${limit}&page=${page}&title=${search}&startDate=${
endDate || "" startDate || ""
}&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${ }&endDate=${endDate || ""}&categoryId=${category || ""}&source=${
sort || "asc" source || ""
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}` }&isPublish=${isPublish !== undefined ? isPublish : ""}&sortBy=${
sortBy || "created_at"
}&sort=${sort || "asc"}&category=${categorySlug || ""}&isBanner=${
isBanner || ""
}`
); );
} }

View File

@ -310,6 +310,7 @@ export type PaginationRequest = {
category?: string; category?: string;
sortBy?: string; sortBy?: string;
sort?: string; sort?: string;
source?: string;
categorySlug?: string; categorySlug?: string;
isBanner?: boolean; isBanner?: boolean;
}; };