diff --git a/public/fonts/Montserrat-Bold.ttf b/public/fonts/Montserrat-Bold.ttf new file mode 100644 index 00000000..02ff6fff Binary files /dev/null and b/public/fonts/Montserrat-Bold.ttf differ diff --git a/public/fonts/Montserrat-Medium.ttf b/public/fonts/Montserrat-Medium.ttf new file mode 100644 index 00000000..dfbcfe4f Binary files /dev/null and b/public/fonts/Montserrat-Medium.ttf differ diff --git a/public/horizontal-white.svg b/public/horizontal-white.svg new file mode 100644 index 00000000..e63d34a6 --- /dev/null +++ b/public/horizontal-white.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/(admin)/admin/posts/[id]/_components/parts/DownloadIGStoryButton.tsx b/src/app/(admin)/admin/posts/[id]/_components/parts/DownloadIGStoryButton.tsx new file mode 100644 index 00000000..b1229201 --- /dev/null +++ b/src/app/(admin)/admin/posts/[id]/_components/parts/DownloadIGStoryButton.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { FaDownload } from "react-icons/fa"; + +export default function DownloadIGStoryButton({ + slug, +}: Readonly<{ slug: string }>) { + const handleDownload = () => { + const link = document.createElement("a"); + link.href = `/api/post/${slug}/instagram-story.png`; + link.download = `${slug}-instagram-story.png`; + link.click(); + }; + + return ( + + ); +} diff --git a/src/app/(admin)/admin/posts/[id]/page.tsx b/src/app/(admin)/admin/posts/[id]/page.tsx index ada21936..28b33707 100644 --- a/src/app/(admin)/admin/posts/[id]/page.tsx +++ b/src/app/(admin)/admin/posts/[id]/page.tsx @@ -8,6 +8,7 @@ import { findAllTags } from "@/utils/database/tag.query"; import EditForm from "./_components/Form"; import PublishButton from "./_components/parts/PublishButton"; +import DownloadIGStoryButton from "./_components/parts/DownloadIGStoryButton"; export const revalidate = 0; @@ -26,8 +27,11 @@ export default async function Edit({ params }: { params: { id: string } }) { return (
-
-

Edit Post

+
+
+

Edit Post

+ +
diff --git a/src/app/(admin)/admin/posts/_components/Table.tsx b/src/app/(admin)/admin/posts/_components/Table.tsx index ec95ed90..0d08838a 100644 --- a/src/app/(admin)/admin/posts/_components/Table.tsx +++ b/src/app/(admin)/admin/posts/_components/Table.tsx @@ -2,7 +2,7 @@ import { useRouter } from "next-nprogress-bar"; import { useEffect, useState } from "react"; import DataTable, { TableColumn } from "react-data-table-component"; -import { FaRegTrashAlt } from "react-icons/fa"; +import { FaRegTrashAlt, FaDownload } from "react-icons/fa"; import { MdPublish, MdUnpublished } from "react-icons/md"; import { toast } from "sonner"; @@ -10,11 +10,33 @@ import { postDelete, updatePostStatus } from "@/actions/post"; import { PostWithTagsAndUser } from "@/types/entityRelations"; import { stringifyCompleteDate } from "@/utils/atomics"; -export default function PostTable({ data }: { data: PostWithTagsAndUser[] }) { +export default function PostTable({ + data, +}: Readonly<{ data: PostWithTagsAndUser[] }>) { const [loader, setLoader] = useState(true); const router = useRouter(); const columns: TableColumn[] = [ + { + name: "Share Image", + cell: (row: PostWithTagsAndUser) => ( + + ), + width: "80px", + sortable: false, + }, { name: "Title", selector: (row: PostWithTagsAndUser) => row.title, @@ -30,7 +52,6 @@ export default function PostTable({ data }: { data: PostWithTagsAndUser[] }) { cell: (row: PostWithTagsAndUser) => ( {row.tags.map((tag) => tag.tagName).join(", ")} ), - sortable: false, }, { @@ -66,14 +87,20 @@ export default function PostTable({ data }: { data: PostWithTagsAndUser[] }) { cell: (row: PostWithTagsAndUser) => (
diff --git a/src/app/(main)/berita/[slug]/_components/BackButton.tsx b/src/app/(main)/berita/[slug]/_components/BackButton.tsx index e63ebce6..030c7063 100644 --- a/src/app/(main)/berita/[slug]/_components/BackButton.tsx +++ b/src/app/(main)/berita/[slug]/_components/BackButton.tsx @@ -11,7 +11,7 @@ export default function GoBack() { return ( ); } diff --git a/src/app/(main)/berita/[slug]/opengraph-image.tsx b/src/app/(main)/berita/[slug]/opengraph-image.tsx new file mode 100644 index 00000000..d365e149 --- /dev/null +++ b/src/app/(main)/berita/[slug]/opengraph-image.tsx @@ -0,0 +1,187 @@ +import { ImageResponse } from "next/og"; +import { ImageResponseOptions } from "next/server"; +import { readFileSync } from "fs"; +import path from "path"; +import { FaPencilAlt } from "react-icons/fa"; +import { CSSProperties } from "react"; +import { findPost } from "@/utils/database/post.query"; +import { stripMarkdown } from "@/utils/atomics"; + +const montserratMedium = readFileSync( + path.join(process.cwd(), "public/fonts/Montserrat-Medium.ttf"), +); + +const montserratBold = readFileSync( + path.join(process.cwd(), "public/fonts/Montserrat-Bold.ttf"), +); + +const styles: Record = { + container: { + display: "flex", + flexDirection: "column" as const, + width: "100%", + height: "100%", + backgroundColor: "#ffffff", + fontFamily: "Montserrat, sans-serif", + position: "relative", + }, + headerImage: { + width: "100%", + height: "500px", + objectFit: "cover" as const, + }, + contentContainer: { + backgroundColor: "#ffffff", + padding: "30px 40px", + display: "flex", + flexDirection: "column" as const, + flex: 1, + }, + tagsContainer: { + display: "flex", + gap: "15px", + marginTop: "5px", + flexWrap: "wrap" as const, + justifyItems: "center", + }, + tag: { + backgroundColor: "#FFF0F0", + color: "#E04E4E", + borderRadius: "20px", + fontSize: 20, + padding: "10px 20px", + }, + articleContainer: { + display: "flex", + flexDirection: "column" as const, + textAlign: "left" as const, + marginTop: 20, + }, + title: { + fontSize: 52, + color: "#000000", + marginBottom: 10, + fontWeight: 700, + textAlign: "left" as const, + lineHeight: 1.2, + }, + content: { + fontSize: 24, + color: "#333333", + marginTop: 10, + marginBottom: 15, + lineHeight: 1.5, + fontWeight: 500, + }, + author: { + fontSize: 22, + marginTop: "15px", + color: "#333333", + fontWeight: 500, + display: "flex", + alignItems: "center", + }, + authorName: { + fontWeight: 700, + marginLeft: "4px", + }, + pencilIcon: { + marginRight: "8px", + }, + footer: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + backgroundColor: "#E04E4E", + color: "#FFF", + padding: "15px 30px", + position: "absolute", + bottom: 0, + width: "100%", + boxSizing: "border-box" as const, + }, + footerLogo: { + height: 45, + }, + footerText: { + fontSize: 24, + fontWeight: 500, + }, +}; + +export const options: ImageResponseOptions = { + width: 1200, + height: 1200, + fonts: [ + { + name: "Montserrat", + data: montserratMedium, + weight: 500, + style: "normal", + }, + { + name: "Montserrat", + data: montserratBold, + weight: 700, + style: "normal", + }, + ], +}; + +export const contentType = "image/png"; + +export default async function opengraphImage({ + params, +}: { + params: { slug: string }; +}) { + const post = await findPost({ slug: params.slug, published: true }); + + if (!post) return

Not Found

; + + const baseUrl = process.env.URL || "https://www.moklet.org"; + + const contentPreview = + stripMarkdown(post.content.split(" ").slice(0, 50).join(" ")) + "..."; + + return new ImageResponse( + ( +
+ Event + +
+
+ {post.tags.slice(0, 3).map((tag) => ( +
+ {tag.tagName} +
+ ))} +
+ +
+

{post.title}

+ +

{contentPreview}

+ +

+ Ditulis oleh + {post.user.name} +

+
+
+ +
+ Moklet.org Logo +

+ Portal menuju kegiatan organisasi kreatif & inovatif di MOKLET +

+
+
+ ), + options, + ); +} diff --git a/src/app/(main)/berita/[slug]/page.tsx b/src/app/(main)/berita/[slug]/page.tsx index 76cadae3..88bff1ad 100644 --- a/src/app/(main)/berita/[slug]/page.tsx +++ b/src/app/(main)/berita/[slug]/page.tsx @@ -31,8 +31,7 @@ export async function generateMetadata({ description: post.description, authors: { name: post.user.name }, openGraph: { - url: `${process.env.URL ?? "https://www.moklet.org/berita/"}${post.slug}`, - images: post.thumbnail, + url: `${process.env.URL ?? "https://www.moklet.org"}/berita/${post.slug}`, title: post.title, description: post.description, type: "article", @@ -47,7 +46,9 @@ export async function generateMetadata({ }; } -export default async function Post({ params }: { params: { slug: string } }) { +export default async function Post({ + params, +}: Readonly<{ params: { slug: string } }>) { const post = await findPost({ slug: params.slug, published: true }); if (!post) notFound(); @@ -60,7 +61,7 @@ export default async function Post({ params }: { params: { slug: string } }) { description: post.description, headline: post.title, datePublished: new Date(post.published_at!).toISOString(), - dateModified: new Date(post.updated_at!).toISOString(), + dateModified: new Date(post.updated_at).toISOString(), thumbnailUrl: post.thumbnail, }; @@ -68,30 +69,32 @@ export default async function Post({ params }: { params: { slug: string } }) {
-
- -
-

{post?.title}

-

{post?.title}

-
-
-
- {post?.user.name - - {trimName(post?.user.name)} - -
-

{stringifyDate(post?.created_at)}

+
+
+ +
+

{post?.title}

+

{post?.title}

+
+
+
+
+
+ {post?.user.name + + {trimName(post?.user.name)} +
-

{post.view_count} views

+

{stringifyDate(post?.created_at)}

+

{post.view_count} views

@@ -113,9 +116,8 @@ export default async function Post({ params }: { params: { slug: string } }) { ))}
@@ -125,8 +127,7 @@ export default async function Post({ params }: { params: { slug: string } }) {

Berita Terkait

- {/* eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain */} - +