diff --git a/package.json b/package.json
index 4b0b043..66b26e6 100644
--- a/package.json
+++ b/package.json
@@ -17,9 +17,11 @@
"@payloadcms/payload-cloud": "^3.35.1",
"@payloadcms/richtext-lexical": "^3.35.1",
"@payloadcms/storage-s3": "^3.35.1",
+ "dayjs": "^1.11.13",
"graphql": "^16.8.1",
"next": "15.3.0",
"payload": "^3.35.1",
+ "qs-esm": "^7.0.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"swiper": "^11.2.6"
@@ -39,4 +41,4 @@
"typescript": "^5"
},
"packageManager": "yarn@4.9.1"
-}
\ No newline at end of file
+}
diff --git a/public/js/script.js b/public/js/script.js
index a24670f..54c4c26 100644
--- a/public/js/script.js
+++ b/public/js/script.js
@@ -1084,91 +1084,91 @@ $(function () {
}
// RD Search
- if (plugins.search.length || plugins.searchResults) {
- var handler = "bat/rd-search.php";
- var defaultTemplate = '
' +
- '...#{token}...
' +
- 'Terms matched: #{count} - URL: #{href}
';
- var defaultFilter = '*.html';
+ // if (plugins.search.length || plugins.searchResults) {
+ // var handler = "bat/rd-search.php";
+ // var defaultTemplate = '' +
+ // '...#{token}...
' +
+ // 'Terms matched: #{count} - URL: #{href}
';
+ // var defaultFilter = '*.html';
- if (plugins.search.length) {
- for (var i = 0; i < plugins.search.length; i++) {
- var searchItem = $(plugins.search[i]),
- options = {
- element: searchItem,
- filter: (searchItem.attr('data-search-filter')) ? searchItem.attr('data-search-filter') : defaultFilter,
- template: (searchItem.attr('data-search-template')) ? searchItem.attr('data-search-template') : defaultTemplate,
- live: (searchItem.attr('data-search-live')) ? searchItem.attr('data-search-live') : false,
- liveCount: (searchItem.attr('data-search-live-count')) ? parseInt(searchItem.attr('data-search-live'), 10) : 4,
- current: 0, processed: 0, timer: {}
- };
+ // if (plugins.search.length) {
+ // for (var i = 0; i < plugins.search.length; i++) {
+ // var searchItem = $(plugins.search[i]),
+ // options = {
+ // element: searchItem,
+ // filter: (searchItem.attr('data-search-filter')) ? searchItem.attr('data-search-filter') : defaultFilter,
+ // template: (searchItem.attr('data-search-template')) ? searchItem.attr('data-search-template') : defaultTemplate,
+ // live: (searchItem.attr('data-search-live')) ? searchItem.attr('data-search-live') : false,
+ // liveCount: (searchItem.attr('data-search-live-count')) ? parseInt(searchItem.attr('data-search-live'), 10) : 4,
+ // current: 0, processed: 0, timer: {}
+ // };
- var $toggle = $('.rd-navbar-search-toggle');
- if ($toggle.length) {
- $toggle.on('click', (function (searchItem) {
- return function () {
- if (!($(this).hasClass('active'))) {
- searchItem.find('input').val('').trigger('propertychange');
- }
- }
- })(searchItem));
- }
+ // var $toggle = $('.rd-navbar-search-toggle');
+ // if ($toggle.length) {
+ // $toggle.on('click', (function (searchItem) {
+ // return function () {
+ // if (!($(this).hasClass('active'))) {
+ // searchItem.find('input').val('').trigger('propertychange');
+ // }
+ // }
+ // })(searchItem));
+ // }
- if (options.live) {
- var clearHandler = false;
+ // if (options.live) {
+ // var clearHandler = false;
- searchItem.find('input').on("input propertychange", $.proxy(function () {
- this.term = this.element.find('input').val().trim();
- this.spin = this.element.find('.input-group-addon');
+ // searchItem.find('input').on("input propertychange", $.proxy(function () {
+ // this.term = this.element.find('input').val().trim();
+ // this.spin = this.element.find('.input-group-addon');
- clearTimeout(this.timer);
+ // clearTimeout(this.timer);
- if (this.term.length > 2) {
- this.timer = setTimeout(liveSearch(this), 200);
+ // if (this.term.length > 2) {
+ // this.timer = setTimeout(liveSearch(this), 200);
- if (clearHandler === false) {
- clearHandler = true;
+ // if (clearHandler === false) {
+ // clearHandler = true;
- $body.on("click", function (e) {
- if ($(e.toElement).parents('.rd-search').length === 0) {
- $('#rd-search-results-live').addClass('cleared').html('');
- }
- })
- }
+ // $body.on("click", function (e) {
+ // if ($(e.toElement).parents('.rd-search').length === 0) {
+ // $('#rd-search-results-live').addClass('cleared').html('');
+ // }
+ // })
+ // }
- } else if (this.term.length === 0) {
- $('#' + this.live).addClass('cleared').html('');
- }
- }, options, this));
- }
+ // } else if (this.term.length === 0) {
+ // $('#' + this.live).addClass('cleared').html('');
+ // }
+ // }, options, this));
+ // }
- searchItem.submit($.proxy(function () {
- $('').attr('type', 'hidden')
- .attr('name', "filter")
- .attr('value', this.filter)
- .appendTo(this.element);
- return true;
- }, options, this))
- }
- }
+ // searchItem.submit($.proxy(function () {
+ // $('').attr('type', 'hidden')
+ // .attr('name', "filter")
+ // .attr('value', this.filter)
+ // .appendTo(this.element);
+ // return true;
+ // }, options, this))
+ // }
+ // }
- if (plugins.searchResults.length) {
- var regExp = /\?.*s=([^&]+)\&filter=([^&]+)/g;
- var match = regExp.exec(location.search);
+ // if (plugins.searchResults.length) {
+ // var regExp = /\?.*s=([^&]+)\&filter=([^&]+)/g;
+ // var match = regExp.exec(location.search);
- if (match !== null) {
- $.get(handler, {
- s: decodeURI(match[1]),
- dataType: "html",
- filter: match[2],
- template: defaultTemplate,
- live: ''
- }, function (data) {
- plugins.searchResults.html(data);
- })
- }
- }
- }
+ // if (match !== null) {
+ // $.get(handler, {
+ // s: decodeURI(match[1]),
+ // dataType: "html",
+ // filter: match[2],
+ // template: defaultTemplate,
+ // live: ''
+ // }, function (data) {
+ // plugins.searchResults.html(data);
+ // })
+ // }
+ // }
+ // }
// Swiper
function makeInterLeaveEffectOptions(interleaveOffset) {
diff --git a/src/app/(main)/blog/page.tsx b/src/app/(main)/blog/page.tsx
index 74e2d91..f1d6f75 100644
--- a/src/app/(main)/blog/page.tsx
+++ b/src/app/(main)/blog/page.tsx
@@ -1,6 +1,5 @@
-import CardBlog from "@/components/CardBlog";
+import ListOfBlog from "@/components/blogs/ListOfBlog";
import HeroImage from "@/components/HeroImage";
-import { CardBlogData } from "@/schema/blog";
import { getDefaultMetadata } from "@/utils/metadata";
import { Metadata } from "next";
@@ -15,57 +14,8 @@ export async function generateMetadata(): Promise {
return metadata;
}
-export default function Blog() {
- const data: CardBlogData[] = [
- {
- slug: "introducing-a-ray-kappe",
- title: "Introducing A Ray Kappe-Designed Masterpiece",
- description:
- "The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
- img: "/images/blog-04-736x540.jpg",
- posted_at: "March 15, 2021",
- },
- {
- slug: "24234",
- title: "Introducing A Ray Kappe-Designed Masterpiece",
- description:
- "The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
- img: "/images/blog-04-736x540.jpg",
- posted_at: "March 15, 2021",
- },
- {
- slug: "wkejrh2k3jr",
- title: "Introducing A Ray Kappe-Designed Masterpiece",
- description:
- "The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
- img: "/images/blog-04-736x540.jpg",
- posted_at: "March 15, 2021",
- },
- {
- slug: "1l2kj4lw34",
- title: "Introducing A Ray Kappe-Designed Masterpiece",
- description:
- "The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
- img: "/images/blog-04-736x540.jpg",
- posted_at: "March 15, 2021",
- },
- {
- slug: "asflj2lj53545",
- title: "Introducing A Ray Kappe-Designed Masterpiece",
- description:
- "The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
- img: "/images/blog-04-736x540.jpg",
- posted_at: "March 15, 2021",
- },
- {
- slug: "adflkj2oj545",
- title: "Introducing A Ray Kappe-Designed Masterpiece",
- description:
- "The famed Cipriani family has unveiled Mr. C Residences, its first-ever hotel branded residences, situated adjacent to the flagship",
- img: "/images/blog-04-736x540.jpg",
- posted_at: "",
- },
- ];
+export default async function Blog({ searchParams }: { searchParams?: Promise<{ s?: string }> }) {
+ const params = await searchParams;
return (
<>
@@ -82,21 +32,14 @@ export default function Blog() {
type="text"
name="s"
autoComplete="off"
+ defaultValue={params?.s}
/>
-
- {data.map((blog) => (
-
- ))}
-
-
-
-
+
+
>
diff --git a/src/collections/Blogs.ts b/src/collections/Blogs.ts
index 55437d6..6a7eea8 100644
--- a/src/collections/Blogs.ts
+++ b/src/collections/Blogs.ts
@@ -99,4 +99,9 @@ export const Blogs: CollectionConfig = {
group: "Blogs",
useAsTitle: "title",
},
+ access: {
+ read: ({ req: { user } }) => {
+ return true;
+ },
+ },
};
diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx
new file mode 100644
index 0000000..0807c0b
--- /dev/null
+++ b/src/components/Loader.tsx
@@ -0,0 +1,17 @@
+export default function Loader() {
+ return (
+
+ );
+}
diff --git a/src/components/CardBlog.tsx b/src/components/blogs/CardBlog.tsx
similarity index 73%
rename from src/components/CardBlog.tsx
rename to src/components/blogs/CardBlog.tsx
index 9c0d163..6d8e299 100644
--- a/src/components/CardBlog.tsx
+++ b/src/components/blogs/CardBlog.tsx
@@ -1,17 +1,20 @@
-import { CardBlogData } from "@/schema/blog";
+import { BlogData } from "@/schema/blog";
import Image from "next/image";
+import Link from "next/link";
type CardBlogProps = {
- data: CardBlogData;
+ data: BlogData;
};
export default function CardBlog({ data }: CardBlogProps) {
return (
-
-
-
+
+
+
+
+
diff --git a/src/components/blogs/ListOfBlog.tsx b/src/components/blogs/ListOfBlog.tsx
new file mode 100644
index 0000000..f979e1d
--- /dev/null
+++ b/src/components/blogs/ListOfBlog.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import Loader from "@/components/Loader";
+import { useBlogQuery } from "@/services/hooks/blog";
+import { useEffect, useRef } from "react";
+import CardBlog from "./CardBlog";
+
+type ListOfBlogProps = {
+ searchKeyword?: string;
+};
+
+export default function ListOfBlog({ searchKeyword }: ListOfBlogProps) {
+ const pageRef = useRef(1);
+ const blogQuery = useBlogQuery();
+
+ useEffect(() => {
+ blogQuery._fetch({
+ search: searchKeyword,
+ page: pageRef.current,
+ });
+ }, []);
+
+ function fetchMore() {
+ blogQuery._fetch({
+ search: searchKeyword,
+ page: ++pageRef.current,
+ });
+ }
+
+ return (
+ <>
+
+ {blogQuery.data.map((blog) => (
+
+ ))}
+
+
+ {blogQuery.isFetching && }
+ {blogQuery.hasNext && (
+
+ )}
+
+ >
+ );
+}
diff --git a/src/payload.config.ts b/src/payload.config.ts
index 83618ec..c53d1fc 100644
--- a/src/payload.config.ts
+++ b/src/payload.config.ts
@@ -18,6 +18,8 @@ const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
export default buildConfig({
+ cors: [process.env.SITE_URL || ""],
+ csrf: [process.env.SITE_URL || ""],
admin: {
user: Users.slug,
importMap: {
diff --git a/src/schema/blog.ts b/src/schema/blog.ts
index 30f066d..6349fc2 100644
--- a/src/schema/blog.ts
+++ b/src/schema/blog.ts
@@ -1,7 +1,7 @@
-export type CardBlogData = {
- slug: string;
+export type BlogData = {
+ slug?: string | null;
title: string;
description: string;
- img: string;
+ img?: { url: string; alt?: string };
posted_at: string;
};
diff --git a/src/schema/services/blog.ts b/src/schema/services/blog.ts
new file mode 100644
index 0000000..ab91539
--- /dev/null
+++ b/src/schema/services/blog.ts
@@ -0,0 +1,6 @@
+export type FetchBlogParams = {
+ page?: number;
+ search?: string;
+ categoryId?: number;
+ tagId?: number;
+};
diff --git a/src/services/hooks/blog.ts b/src/services/hooks/blog.ts
new file mode 100644
index 0000000..ba9f31e
--- /dev/null
+++ b/src/services/hooks/blog.ts
@@ -0,0 +1,28 @@
+import { BlogData } from "@/schema/blog";
+import { FetchBlogParams } from "@/schema/services/blog";
+import { useState } from "react";
+import { fetchBlogREST } from "../rest/blog";
+
+export function useBlogQuery() {
+ const [data, setData] = useState([]);
+ const [isFetching, setFetching] = useState(false);
+ const [hasNext, setHasNext] = useState(false);
+
+ async function _fetch(params: FetchBlogParams = {}) {
+ setFetching(true);
+ const res = await fetchBlogREST(params);
+ setFetching(false);
+
+ if (Array.isArray(res?.formattedData)) {
+ setData(res.formattedData);
+ }
+ setHasNext(res?.hasNextPage ?? false);
+ }
+
+ return {
+ _fetch,
+ data,
+ isFetching,
+ hasNext,
+ };
+}
diff --git a/src/services/payload/blog.ts b/src/services/payload/blog.ts
new file mode 100644
index 0000000..bd4696d
--- /dev/null
+++ b/src/services/payload/blog.ts
@@ -0,0 +1,147 @@
+import payloadConfig from "@/payload.config";
+import { FetchBlogParams } from "@/schema/services/blog";
+import { formatDate } from "@/utils/datetime";
+import { getRandomNumber } from "@/utils/general";
+import { getPayload, Where } from "payload";
+
+export async function fetchBlog({ page, search = "", categoryId, tagId }: FetchBlogParams = {}) {
+ const payload = await getPayload({ config: payloadConfig });
+
+ const queryCondition: Where = {
+ _status: { equals: "published" },
+ };
+
+ if (!!search) {
+ queryCondition["title"] = {
+ contains: search,
+ };
+ }
+ if (!!categoryId) {
+ queryCondition["categories"] = {
+ equals: categoryId,
+ };
+ }
+ if (!!tagId) {
+ queryCondition["tags"] = {
+ equals: tagId,
+ };
+ }
+
+ const blogDataQuery = await payload.find({
+ collection: "blogs",
+ page,
+ pagination: true,
+ limit: 9,
+ where: queryCondition,
+ });
+
+ const formattedData = blogDataQuery.docs.map((item) => {
+ return {
+ ...item,
+ imgFormatted: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined,
+ createdAtFormatted: formatDate(item.createdAt),
+ };
+ });
+
+ return {
+ ...blogDataQuery,
+ formattedData,
+ };
+}
+
+export async function fetchBlogSuggestion() {
+ const payload = await getPayload({ config: payloadConfig });
+ const limitPerPage = 2;
+ const blogCountQuery = await payload.count({
+ collection: "blogs",
+ where: { _status: { equals: "published" } },
+ });
+
+ // randomize page
+ let page = 1;
+ const totalDocs = blogCountQuery.totalDocs;
+ if (totalDocs > limitPerPage) {
+ const totalPage = Math.ceil(totalDocs / limitPerPage);
+ page = getRandomNumber(totalPage);
+ }
+
+ const blogDataQuery = await payload.find({
+ collection: "blogs",
+ page,
+ limit: limitPerPage,
+ });
+
+ const formattedData = blogDataQuery.docs.map((item) => {
+ return {
+ ...item,
+ imgFormatted: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined,
+ createdAtFormatted: formatDate(item.createdAt),
+ };
+ });
+
+ return {
+ ...blogDataQuery,
+ formattedData,
+ };
+}
+
+export async function fetchBlogDetail(slug: string | undefined) {
+ const payload = await getPayload({ config: payloadConfig });
+ const blogDataQuery = await payload.find({
+ collection: "blogs",
+ where: {
+ _status: { equals: "published" },
+ slug: { equals: slug },
+ },
+ limit: 1,
+ pagination: false,
+ });
+
+ if (!blogDataQuery?.docs?.[0]) return null;
+
+ const data = blogDataQuery?.docs?.[0];
+ const createdAt = formatDate(data.createdAt);
+ const updatedAt = formatDate(data.updatedAt);
+ const imgUrl = typeof data.img !== "number" ? (data?.img?.url ?? "") : "";
+
+ return {
+ data,
+ createdAt,
+ updatedAt,
+ imgUrl,
+ };
+}
+
+export async function fetchBlogCategoryBySlug(slug: string) {
+ const payload = await getPayload({ config: payloadConfig });
+ const category = await payload.find({
+ collection: "blogCategories",
+ where: {
+ _status: { equals: "published" },
+ slug: { equals: slug },
+ },
+ });
+
+ if (!category?.docs?.[0]) return null;
+
+ return {
+ data: category.docs[0],
+ };
+}
+
+export async function fetchBlogTagBySlug(slug: string) {
+ const payload = await getPayload({ config: payloadConfig });
+ const tag = await payload.find({
+ collection: "blogTags",
+ where: {
+ _status: { equals: "published" },
+ slug: { equals: slug },
+ },
+ });
+
+ if (!tag?.docs?.[0]) return null;
+
+ return {
+ data: tag.docs[0],
+ };
+}
diff --git a/src/services/rest/blog.ts b/src/services/rest/blog.ts
new file mode 100644
index 0000000..1ea4527
--- /dev/null
+++ b/src/services/rest/blog.ts
@@ -0,0 +1,61 @@
+import { Blog } from "@/payload-types";
+import { BlogData } from "@/schema/blog";
+import { FetchBlogParams } from "@/schema/services/blog";
+import { formatDate } from "@/utils/datetime";
+import { sanitizeBlogContentIntoStringPreview } from "@/utils/sanitize";
+import { PaginatedDocs, Where } from "payload";
+import { stringify } from "qs-esm";
+
+export async function fetchBlogREST({ page, search = "", categoryId, tagId }: FetchBlogParams = {}) {
+ const queryCondition: Where = {
+ _status: { equals: "published" },
+ };
+
+ if (!!search) {
+ queryCondition["title"] = {
+ contains: search,
+ };
+ }
+ if (!!categoryId) {
+ queryCondition["categories"] = {
+ equals: categoryId,
+ };
+ }
+ if (!!tagId) {
+ queryCondition["tags"] = {
+ equals: tagId,
+ };
+ }
+
+ const queryParams = stringify(
+ {
+ page,
+ pagination: true,
+ limit: 9,
+ where: queryCondition,
+ },
+ { addQueryPrefix: true }
+ );
+
+ const blogRequest = await fetch(`/api/blogs${queryParams}`);
+
+ if (blogRequest.ok) {
+ const resData = (await blogRequest.json()) as PaginatedDocs;
+ const formattedData: BlogData[] = resData.docs.map((item) => {
+ return {
+ slug: item.slug,
+ title: item.title,
+ description: sanitizeBlogContentIntoStringPreview(item.content),
+ img: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined,
+ posted_at: formatDate(item.createdAt),
+ };
+ });
+
+ return {
+ ...resData,
+ formattedData,
+ };
+ } else {
+ return null;
+ }
+}
diff --git a/src/utils/datetime.ts b/src/utils/datetime.ts
new file mode 100644
index 0000000..f48879e
--- /dev/null
+++ b/src/utils/datetime.ts
@@ -0,0 +1,5 @@
+import dayjs from "dayjs";
+
+export function formatDate(iso: string, format: string = "MMM, D YYYY") {
+ return dayjs(iso).format(format);
+}
diff --git a/src/utils/general.ts b/src/utils/general.ts
new file mode 100644
index 0000000..294ef90
--- /dev/null
+++ b/src/utils/general.ts
@@ -0,0 +1,7 @@
+export function limitString(text: string) {
+ return `${text.length > 100 ? `${text.slice(0, 100)}...` : text}`;
+}
+
+export function getRandomNumber(range: number): number {
+ return Math.floor(Math.random() * range) + 1;
+}
diff --git a/src/utils/sanitize.ts b/src/utils/sanitize.ts
new file mode 100644
index 0000000..34432f6
--- /dev/null
+++ b/src/utils/sanitize.ts
@@ -0,0 +1,32 @@
+import { Blog } from "@/payload-types";
+
+export function sanitizePageNumber(page: any, defaultPage = 1): number {
+ const parsedPage = Number(page);
+
+ if (isNaN(parsedPage) || parsedPage < 1 || !Number.isInteger(parsedPage)) {
+ return defaultPage;
+ }
+
+ return parsedPage;
+}
+
+export function sanitizeBlogContentIntoStringPreview(data: Blog["content"]) {
+ // Find the first paragraph that has children with text
+ const firstParagraph = data.root.children.find(
+ (node) =>
+ node.type === "paragraph" &&
+ Array.isArray(node.children) &&
+ node.children.length > 0 &&
+ !!node.children?.[0]?.text
+ );
+
+ if (!firstParagraph) {
+ return "...";
+ }
+
+ // @ts-ignore
+ const text = firstParagraph.children?.[0]?.text ?? "";
+
+ // Limit to 100 characters
+ return `${text.length > 100 ? text.slice(0, 100) : text}...`;
+}
diff --git a/yarn.lock b/yarn.lock
index 492728a..7579afd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4759,6 +4759,13 @@ __metadata:
languageName: node
linkType: hard
+"dayjs@npm:^1.11.13":
+ version: 1.11.13
+ resolution: "dayjs@npm:1.11.13"
+ checksum: 10c0/a3caf6ac8363c7dade9d1ee797848ddcf25c1ace68d9fe8678ecf8ba0675825430de5d793672ec87b24a69bf04a1544b176547b2539982275d5542a7955f35b7
+ languageName: node
+ linkType: hard
+
"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.0":
version: 4.4.0
resolution: "debug@npm:4.4.0"
@@ -5015,6 +5022,7 @@ __metadata:
"@types/node": "npm:^20"
"@types/react": "npm:^19"
"@types/react-dom": "npm:^19"
+ dayjs: "npm:^1.11.13"
eslint: "npm:^9"
eslint-config-next: "npm:15.3.0"
eslint-config-prettier: "npm:^10.1.2"
@@ -5023,6 +5031,7 @@ __metadata:
next: "npm:15.3.0"
payload: "npm:^3.35.1"
prettier: "npm:^3.5.3"
+ qs-esm: "npm:^7.0.2"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
swiper: "npm:^11.2.6"
@@ -8673,7 +8682,7 @@ __metadata:
languageName: node
linkType: hard
-"qs-esm@npm:7.0.2":
+"qs-esm@npm:7.0.2, qs-esm@npm:^7.0.2":
version: 7.0.2
resolution: "qs-esm@npm:7.0.2"
checksum: 10c0/b46e15883b91818fd6b0862cac97439dfe67a1401c00729756b16463fa97e094239017dd4f17369dd0cf586e262305b165ee485c0b1088ca4d2eb7ad11c0c8fe