feat: single page payload integration fetching from db
This commit is contained in:
parent
5240b07145
commit
06057b51db
5
src/app/(main)/[slug]/loading.tsx
Normal file
5
src/app/(main)/[slug]/loading.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import LoaderFixed from "@/components/loaders/LoaderFixed";
|
||||
|
||||
export default function Loading() {
|
||||
return <LoaderFixed />;
|
||||
}
|
111
src/app/(main)/[slug]/page.tsx
Normal file
111
src/app/(main)/[slug]/page.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import DetailPageBlog from "@/components/blogs/DetailPageBlog";
|
||||
import DetailPage from "@/components/pages/DetailPage";
|
||||
import { fetchBlogDetail } from "@/services/payload/blog";
|
||||
import { fetchPageBySlug } from "@/services/payload/page";
|
||||
import { getDefaultMetadata } from "@/utils/metadata";
|
||||
import { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export async function generateMetadata(props: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||
const metadata = await getDefaultMetadata();
|
||||
const params = await props.params;
|
||||
|
||||
let title = `Page Not Found - ${metadata.openGraph?.siteName}`;
|
||||
let description = title;
|
||||
let publishedAt = "";
|
||||
let updatedAt = "";
|
||||
let imgUrl = "";
|
||||
let createdByName = "";
|
||||
let canonicalUrl = "";
|
||||
|
||||
const blog = await fetchBlogDetail(params.slug);
|
||||
if (!!blog) {
|
||||
// check for blog data
|
||||
title = `${!!blog.data?.meta?.title ? blog.data?.meta?.title : blog.data.title} - ${metadata.openGraph?.siteName}`;
|
||||
description = `${!!blog.data?.meta?.description ? blog.data?.meta?.description : blog.data.title}`;
|
||||
imgUrl = blog.img.url;
|
||||
publishedAt = blog.data.createdAt;
|
||||
updatedAt = blog.data.updatedAt;
|
||||
if (!!blog.data?.meta?.canonical_url) {
|
||||
canonicalUrl = blog.data.meta.canonical_url;
|
||||
}
|
||||
if (!!blog?.data?.createdBy && typeof blog.data.createdBy !== "number") {
|
||||
createdByName = blog.data.createdBy?.name ?? "";
|
||||
}
|
||||
} else {
|
||||
// check for page data when blog is not found
|
||||
const page = await fetchPageBySlug({ slug: params.slug });
|
||||
if (!!page) {
|
||||
title = `${!!page?.data?.meta?.title ? page?.data?.meta?.title : page.data.title} - ${metadata.openGraph?.siteName}`;
|
||||
description = `${!!page?.data?.meta?.description ? page?.data?.meta?.description : page.data.title}`;
|
||||
imgUrl = page.heroImg?.url;
|
||||
publishedAt = page.createdAt;
|
||||
updatedAt = page.updatedAt;
|
||||
if (!!page.data?.meta?.canonical_url) {
|
||||
canonicalUrl = page.data.meta.canonical_url;
|
||||
}
|
||||
if (!!page?.data?.createdBy && typeof page?.data?.createdBy !== "number") {
|
||||
createdByName = page?.data?.createdBy?.name ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metadata.title = title;
|
||||
metadata.description = description;
|
||||
if (!!metadata.openGraph) {
|
||||
// @ts-ignore
|
||||
metadata.openGraph.type = "article";
|
||||
metadata.openGraph.title = title;
|
||||
metadata.openGraph.description = description;
|
||||
metadata.openGraph.images = !!imgUrl ? [imgUrl] : undefined;
|
||||
}
|
||||
if (!!metadata.alternates && !!canonicalUrl) {
|
||||
metadata.alternates.canonical = canonicalUrl;
|
||||
}
|
||||
metadata.twitter = {
|
||||
card: "summary_large_image",
|
||||
title: title,
|
||||
description: description,
|
||||
images: !!imgUrl ? [imgUrl] : undefined,
|
||||
};
|
||||
metadata.other = {
|
||||
"article:published_time": publishedAt,
|
||||
"article:modified_time": updatedAt,
|
||||
"twitter:label1": "Written by",
|
||||
"twitter:data1": !!createdByName ? createdByName : "Admin",
|
||||
"twitter:label2": "Est. reading time",
|
||||
"twitter:data2": "3 minutes",
|
||||
};
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export default async function SinglePage(props: { params: Promise<{ slug: string }> }) {
|
||||
const params = await props.params;
|
||||
const headersList = await headers();
|
||||
const fullUrl = headersList.get("x-full-url");
|
||||
const shareUrl = {
|
||||
facebook: `https://www.facebook.com/sharer/sharer.php?u=${fullUrl}`,
|
||||
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${fullUrl}`,
|
||||
twitter: `https://twitter.com/intent/tweet?url=${fullUrl}`,
|
||||
};
|
||||
|
||||
const blog = await fetchBlogDetail(params.slug);
|
||||
if (!!blog) {
|
||||
return (
|
||||
<>
|
||||
<DetailPageBlog data={blog} shareUrl={shareUrl} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const page = await fetchPageBySlug({ slug: params.slug });
|
||||
if (!page) return notFound();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailPage data={page} shareUrl={shareUrl} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,19 +1,16 @@
|
||||
import ListOfRecentBlog from "@/components/blogs/ListOfRecentBlog";
|
||||
import HeroImage from "@/components/HeroImage";
|
||||
import DetailPageBlog from "@/components/blogs/DetailPageBlog";
|
||||
import { fetchBlogDetail } from "@/services/payload/blog";
|
||||
import { getDefaultMetadata } from "@/utils/metadata";
|
||||
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||
import { Metadata } from "next";
|
||||
import { headers } from "next/headers";
|
||||
import Image from "next/image";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export async function generateMetadata(props: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||
const metadata = await getDefaultMetadata();
|
||||
const params = await props.params;
|
||||
|
||||
let title = "Page";
|
||||
let description = "Page";
|
||||
let title = `Page Not Found - ${metadata.openGraph?.siteName}`;
|
||||
let description = title;
|
||||
let publishedAt = "";
|
||||
let updatedAt = "";
|
||||
let imgUrl = "";
|
||||
@ -81,135 +78,7 @@ export default async function BlogDetail(props: { params: Promise<{ slug: string
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeroImage title={blog.data.title} />
|
||||
|
||||
<section className="section section-md bg-colorSection1">
|
||||
<div className="container">
|
||||
<div className="row justify-content-lg-center">
|
||||
<div className="col-lg-8">
|
||||
<article className="blog-post-solo">
|
||||
<div className="blog-post-solo-part">
|
||||
<div className="w-full h-56 md:h-80 bg-colorImgPlaceholder/50 relative">
|
||||
<Image className="object-cover" src={blog.img.url} alt={blog.img.alt} fill />
|
||||
</div>
|
||||
|
||||
<RichText className="mt-4" data={blog.data.content} />
|
||||
</div>
|
||||
|
||||
<div className="blog-post-solo-footer">
|
||||
<div className="blog-post-solo-footer-left">
|
||||
<ul className="blog-post-solo-footer-list">
|
||||
<li>
|
||||
<span className="icon mdi mdi-clock"></span>
|
||||
<a href="#">{blog.createdAt}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="blog-post-solo-footer-right">
|
||||
<ul className="blog-post-solo-footer-list-1">
|
||||
<li>
|
||||
<span>Share this post</span>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
className="icon icon-circle icon-rounded icon-5 fa-facebook"
|
||||
href={shareUrl.facebook}
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
className="icon icon-circle icon-rounded icon-6 fa-twitter"
|
||||
href={shareUrl.twitter}
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
className="icon icon-circle icon-rounded icon-4 fa-linkedin"
|
||||
href={shareUrl.linkedin}
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ListOfRecentBlog currentBlogId={blog?.data?.id} />
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="col-lg-4">
|
||||
<div className="pdl-xl-40">
|
||||
<div className="row row-60">
|
||||
<div className="col-md-6 col-lg-12">
|
||||
<form action="/blog" className="form-lg rd-search rd-search-classic">
|
||||
<div className="form-wrap">
|
||||
<input
|
||||
className="form-input"
|
||||
id="rd-search-form-input"
|
||||
type="text"
|
||||
name="s"
|
||||
autoComplete="off"
|
||||
placeholder="Search blog..."
|
||||
/>
|
||||
</div>
|
||||
<button className="rd-search-submit" type="submit"></button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="col-md-6 col-lg-12">
|
||||
<div className="block-info-2">
|
||||
<div className="block-info-2-title">
|
||||
<h3>Latest Listings</h3>
|
||||
</div>
|
||||
<a className="post-minimal-1" href="#">
|
||||
<div className="post-minimal-1-left">
|
||||
<Image src="/images/post-agent-01-212x208.jpg" alt="" width="212" height="208" />
|
||||
</div>
|
||||
<div className="post-minimal-1-body">
|
||||
<div className="post-minimal-1-title">
|
||||
<span>401 Biscayne Blvd</span>
|
||||
</div>
|
||||
<div className="post-minimal-1-text">
|
||||
<span>$5000\mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a className="post-minimal-1" href="#">
|
||||
<div className="post-minimal-1-left">
|
||||
<Image src="/images/post-agent-02-212x208.jpg" alt="" width="212" height="208" />
|
||||
</div>
|
||||
<div className="post-minimal-1-body">
|
||||
<div className="post-minimal-1-title">
|
||||
<span>35 Pond St, New York</span>
|
||||
</div>
|
||||
<div className="post-minimal-1-text">
|
||||
<span>$5550\mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a className="post-minimal-1" href="#">
|
||||
<div className="post-minimal-1-left">
|
||||
<Image src="/images/post-agent-03-212x208.jpg" alt="" width="212" height="208" />
|
||||
</div>
|
||||
<div className="post-minimal-1-body">
|
||||
<div className="post-minimal-1-title">
|
||||
<span>182 3rd St, Seattle</span>
|
||||
</div>
|
||||
<div className="post-minimal-1-text">
|
||||
<span>$2520\mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<DetailPageBlog data={blog} shareUrl={shareUrl} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ type HeroImageProps = {
|
||||
|
||||
export default function HeroImage({ title = "", imgSrc = "/images/breadcrumbs-bg-05-1922x441.jpg" }: HeroImageProps) {
|
||||
return (
|
||||
<section className="breadcrumbs-custom bg-image context-dark">
|
||||
<section className="breadcrumbs-custom context-dark min-h-[300px]!">
|
||||
<Image
|
||||
alt="Blog"
|
||||
src={imgSrc}
|
||||
|
@ -4,23 +4,9 @@ import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||
|
||||
export function ContentBlock(props: any) {
|
||||
return (
|
||||
<div className="container relative">
|
||||
<div className="row">
|
||||
{/* Content */}
|
||||
<div className="col-md-10 offset-md-1 col-lg-10 offset-lg-1">
|
||||
{/* Post */}
|
||||
<div className="blog-item mb-10">
|
||||
<div className="blog-item-body">
|
||||
<div>
|
||||
{/* @ts-ignore */}
|
||||
<RichText data={props.content} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Post */}
|
||||
</div>
|
||||
{/* End Content */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ type CardBlogProps = {
|
||||
};
|
||||
|
||||
export default function CardBlog({ data, colorPreset = 1, isDescriptionVisible = true }: CardBlogProps) {
|
||||
const linkDetail = `/blog/${data.slug}`;
|
||||
const linkDetail = `/${data.slug}`;
|
||||
|
||||
if (colorPreset === 2) {
|
||||
return (
|
||||
|
151
src/components/blogs/DetailPageBlog.tsx
Normal file
151
src/components/blogs/DetailPageBlog.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import { fetchBlogDetail } from "@/services/payload/blog";
|
||||
import Image from "next/image";
|
||||
import ListOfRecentBlog from "./ListOfRecentBlog";
|
||||
import HeroImage from "../HeroImage";
|
||||
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||
|
||||
type shareUrlDestination = "facebook" | "linkedin" | "twitter";
|
||||
|
||||
type DetailPageBlogProps = {
|
||||
data: Awaited<ReturnType<typeof fetchBlogDetail>>;
|
||||
shareUrl: Record<shareUrlDestination, string>;
|
||||
};
|
||||
|
||||
export default function DetailPageBlog({ data, shareUrl }: DetailPageBlogProps) {
|
||||
const blog = data;
|
||||
if (!blog) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeroImage title={blog.data.title} />
|
||||
|
||||
<section className="section section-md bg-colorSection1">
|
||||
<div className="container">
|
||||
<div className="row justify-content-lg-center">
|
||||
<div className="col-lg-8">
|
||||
<article className="blog-post-solo">
|
||||
<div className="blog-post-solo-part">
|
||||
<div className="w-full h-56 md:h-80 bg-colorImgPlaceholder/50 relative">
|
||||
<Image className="object-cover" src={blog.img.url} alt={blog.img.alt} fill />
|
||||
</div>
|
||||
|
||||
<RichText className="mt-4" data={blog.data.content} />
|
||||
</div>
|
||||
|
||||
<div className="blog-post-solo-footer">
|
||||
<div className="blog-post-solo-footer-left">
|
||||
<ul className="blog-post-solo-footer-list">
|
||||
<li>
|
||||
<span className="icon mdi mdi-clock"></span>
|
||||
<a href="#">{blog.createdAt}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="blog-post-solo-footer-right">
|
||||
<ul className="blog-post-solo-footer-list-1">
|
||||
<li>
|
||||
<span>Share this post</span>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
className="icon icon-circle icon-rounded icon-5 fa-facebook"
|
||||
href={shareUrl.facebook}
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
className="icon icon-circle icon-rounded icon-6 fa-twitter"
|
||||
href={shareUrl.twitter}
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
className="icon icon-circle icon-rounded icon-4 fa-linkedin"
|
||||
href={shareUrl.linkedin}
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ListOfRecentBlog currentBlogId={blog?.data?.id} />
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="col-lg-4">
|
||||
<div className="pdl-xl-40">
|
||||
<div className="row row-60">
|
||||
<div className="col-md-6 col-lg-12">
|
||||
<form action="/blog" className="form-lg rd-search rd-search-classic">
|
||||
<div className="form-wrap">
|
||||
<input
|
||||
className="form-input"
|
||||
id="rd-search-form-input"
|
||||
type="text"
|
||||
name="s"
|
||||
autoComplete="off"
|
||||
placeholder="Search blog..."
|
||||
/>
|
||||
</div>
|
||||
<button className="rd-search-submit" type="submit"></button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="col-md-6 col-lg-12">
|
||||
<div className="block-info-2">
|
||||
<div className="block-info-2-title">
|
||||
<h3>Latest Listings</h3>
|
||||
</div>
|
||||
<a className="post-minimal-1" href="#">
|
||||
<div className="post-minimal-1-left">
|
||||
<Image src="/images/post-agent-01-212x208.jpg" alt="" width="212" height="208" />
|
||||
</div>
|
||||
<div className="post-minimal-1-body">
|
||||
<div className="post-minimal-1-title">
|
||||
<span>401 Biscayne Blvd</span>
|
||||
</div>
|
||||
<div className="post-minimal-1-text">
|
||||
<span>$5000\mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a className="post-minimal-1" href="#">
|
||||
<div className="post-minimal-1-left">
|
||||
<Image src="/images/post-agent-02-212x208.jpg" alt="" width="212" height="208" />
|
||||
</div>
|
||||
<div className="post-minimal-1-body">
|
||||
<div className="post-minimal-1-title">
|
||||
<span>35 Pond St, New York</span>
|
||||
</div>
|
||||
<div className="post-minimal-1-text">
|
||||
<span>$5550\mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a className="post-minimal-1" href="#">
|
||||
<div className="post-minimal-1-left">
|
||||
<Image src="/images/post-agent-03-212x208.jpg" alt="" width="212" height="208" />
|
||||
</div>
|
||||
<div className="post-minimal-1-body">
|
||||
<div className="post-minimal-1-title">
|
||||
<span>182 3rd St, Seattle</span>
|
||||
</div>
|
||||
<div className="post-minimal-1-text">
|
||||
<span>$2520\mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
@ -105,7 +105,7 @@ export default async function Footer() {
|
||||
<span>Dynamic Realty</span> <span>© </span>
|
||||
<span className="copyright-year"></span>
|
||||
<span> </span>
|
||||
<a href="privacy-policy.html">Privacy Policy</a>
|
||||
<a href="/privacy-policy">Privacy Policy</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-sm-6 text-sm-right">
|
||||
|
74
src/components/pages/DetailPage.tsx
Normal file
74
src/components/pages/DetailPage.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { fetchPageBySlug } from "@/services/payload/page";
|
||||
import HeroImage from "../HeroImage";
|
||||
import { RenderBlocks } from "../blocks/RenderBlocks";
|
||||
|
||||
type shareUrlDestination = "facebook" | "linkedin" | "twitter";
|
||||
|
||||
type DetailPageProps = {
|
||||
data: Awaited<ReturnType<typeof fetchPageBySlug>>;
|
||||
shareUrl: Record<shareUrlDestination, string>;
|
||||
};
|
||||
|
||||
export default function DetailPage({ data, shareUrl }: DetailPageProps) {
|
||||
const page = data;
|
||||
if (!page) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeroImage imgSrc={page.heroImg?.url} title={page.data.title} />
|
||||
|
||||
<section className="section section-md bg-colorSection1">
|
||||
<div className="container">
|
||||
<div className="row justify-content-lg-center">
|
||||
<div className="col-12">
|
||||
<article className="blog-post-solo">
|
||||
<div className="blog-post-solo-part">
|
||||
<RenderBlocks blocks={page.data.layout} />
|
||||
</div>
|
||||
|
||||
<div className="blog-post-solo-footer">
|
||||
<div className="blog-post-solo-footer-left">
|
||||
<ul className="blog-post-solo-footer-list">
|
||||
<li>
|
||||
<span className="icon mdi mdi-clock"></span>
|
||||
<a href="#">{page.createdAt}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="blog-post-solo-footer-right">
|
||||
<ul className="blog-post-solo-footer-list-1">
|
||||
<li>
|
||||
<span>Share this post</span>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
className="icon icon-circle icon-rounded icon-5 fa-facebook"
|
||||
href={shareUrl.facebook}
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
className="icon icon-circle icon-rounded icon-6 fa-twitter"
|
||||
href={shareUrl.twitter}
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
className="icon icon-circle icon-rounded icon-4 fa-linkedin"
|
||||
href={shareUrl.linkedin}
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
40
src/services/payload/page.ts
Normal file
40
src/services/payload/page.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import payloadConfig from "@/payload.config";
|
||||
import { formatDate } from "@/utils/datetime";
|
||||
import { getPayload } from "payload";
|
||||
|
||||
export const fetchPageBySlug = async ({ slug }: { slug: string | undefined }) => {
|
||||
const payload = await getPayload({ config: payloadConfig });
|
||||
|
||||
const result = await payload.find({
|
||||
collection: "pages",
|
||||
// draft,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
// overrideAccess: draft,
|
||||
where: {
|
||||
_status: { equals: "published" },
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.docs?.[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = result.docs[0];
|
||||
|
||||
const heroImgUrl = typeof data.hero_img !== "number" ? (data?.hero_img?.url ?? "") : "";
|
||||
const heroImgAlt = typeof data.hero_img !== "number" ? (data?.hero_img?.alt ?? "") : "";
|
||||
|
||||
return {
|
||||
data: data,
|
||||
createdAt: formatDate(data.createdAt),
|
||||
updatedAt: formatDate(data.updatedAt),
|
||||
heroImg: {
|
||||
url: heroImgUrl,
|
||||
alt: heroImgAlt,
|
||||
},
|
||||
};
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user