Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP - Blog extensions: Reviews and Ratings #861

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
aafa26c
feat(blogreaction): setup started
Sep 13, 2024
eab7642
Merge branch 'main' of github.com:deco-cx/apps into feat-blog-like
Sep 13, 2024
5b41f68
chore(blogLike): reaction mock changed
Sep 16, 2024
c8d9b2d
feat(blogLike): deco records bug fix
Sep 16, 2024
a563bc5
feat(bloglike): extension loaders ended
Sep 16, 2024
6401764
feat(bloglike): submit extension
Sep 16, 2024
64a827d
fix(bloglike): action fix
Sep 17, 2024
e85fab2
chore(bloglike): prettify code
Sep 17, 2024
d6e7437
feat(blogpost): comment feat started
Sep 17, 2024
e348e9f
feat(blogpost): comment action added
Sep 17, 2024
49ad269
Merge pull request #868 from deco-cx/feat-blog-comment
aka-sacci-ccr Sep 17, 2024
b26b7f6
fix(blog): changed schema
Sep 18, 2024
8af05b2
Merge branch 'main' of github.com:deco-cx/apps into feat-blog-comment
Sep 18, 2024
214d7b7
Merge branch 'main' of github.com:deco-cx/apps into feat-blog-like
Sep 18, 2024
62983ae
Merge branch 'feat-blog-like' of github.com:deco-cx/apps into feat-bl…
Sep 18, 2024
7d68653
Merge branch 'feat-blog-like' of github.com:deco-cx/apps into feat-bl…
Sep 18, 2024
164649b
Merge pull request #870 from deco-cx/feat-blog-comment
aka-sacci-ccr Sep 18, 2024
e270c56
feat(blog): extension props
Sep 18, 2024
29026f7
Merge branch 'feat-blog-like' of github.com:deco-cx/apps into feat-bl…
Sep 18, 2024
c943a79
Merge pull request #871 from deco-cx/feat-blog-extesions
aka-sacci-ccr Sep 18, 2024
dba5261
Merge branch 'main' of github.com:deco-cx/apps into feat-blog-like
Sep 23, 2024
8885171
feat(blogpost): new types for carousel
Sep 23, 2024
1695e4e
Merge branch 'main' of github.com:deco-cx/apps into feat-blog-like
Sep 30, 2024
861292a
Merge branch 'main' of github.com:deco-cx/apps into feat-blog-like
Oct 7, 2024
6627bf4
feat(blogextensions): update database script
Oct 7, 2024
af2a955
feat(blogextensions): script workaround
Oct 7, 2024
76b6520
Merge branch 'main' of github.com:deco-cx/apps into feat-blog-like
Oct 11, 2024
1791251
Merge branch 'main' of github.com:deco-cx/apps into feat-blog-like
Oct 15, 2024
2cd96b8
Merge branch 'main' of github.com:deco-cx/apps into feat-blog-like
Oct 21, 2024
b49c320
Merge branch 'main' of github.com:deco-cx/apps into feat-blog-like
Oct 30, 2024
e5ebdde
feat(most-viewed): order by most viewed started
Oct 30, 2024
6ccb944
feat(most-viewed): loader and orderBy finished
Oct 30, 2024
5d46609
feat(most-viewed): action finished
Oct 30, 2024
ff7a1d0
chore(blog): blogposting core logic archives
Oct 31, 2024
f9b53bd
Merge branch 'main' of github.com:deco-cx/apps into feat-blog-like
Nov 27, 2024
863cc25
feat(blogpost): listing now supports slugs lists
Nov 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions blog/actions/submitRating.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { and, eq, like, or } from "https://esm.sh/[email protected]";
import { Person } from "../../commerce/types.ts";
import { AppContext } from "../mod.ts";
import { logger } from "@deco/deco/o11y";
import { Rating } from "../types.ts";
import { rating } from "../db/schema.ts";

export interface Props {
itemReviewed: string;
author: Person;
ratingValue: number;
additionalType?: string;
}

export default async function submitRating(
{ itemReviewed, author, ratingValue, additionalType }: Props,
_req: Request,
ctx: AppContext,
): Promise<Rating | null> {
const records = await ctx.invoke.records.loaders.drizzle();

try {
const storedRating = await records.select({
id: rating.id,
itemReviewed: rating.itemReviewed,
author: rating.author,
ratingValue: rating.ratingValue,
additionalType: rating.additionalType,
})
.from(rating).where(
and(
eq(rating.itemReviewed, itemReviewed),
or(
like(rating.author, `%"email":"${author.email}"%`),
like(rating.author, `%"id":"${author["@id"]}"%`),
),
),
) as Rating[] | undefined;

//if has data, then update de table
if (storedRating && storedRating.length > 0 && storedRating?.at(0)?.id) {
const current = storedRating.at(0)!;
await records.update(rating).set({
ratingValue,
additionalType: additionalType ?? current.additionalType,
}).where(
eq(rating.id, current.id!),
);
return {
...current,
ratingValue,
additionalType: additionalType ?? current.additionalType,
};
}

const insertedData = {
itemReviewed,
author: author!,
ratingValue: ratingValue!,
additionalType: additionalType,
};

await records.insert(rating).values({
...insertedData,
});

return {
"@type": "Rating",
...insertedData,
};
} catch (e) {
logger.error(e);
return null;
}
}
87 changes: 87 additions & 0 deletions blog/actions/submitReview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { eq } from "https://esm.sh/[email protected]";
import { Person } from "../../commerce/types.ts";
import { AppContext } from "../mod.ts";
import { logger } from "@deco/deco/o11y";
import { Review } from "../types.ts";
import { getReviewById } from "../core/records.ts";
import { review } from "../db/schema.ts";

export interface Props {
action: "create" | "update";
id?: string;
reviewBody?: string;
reviewHeadline?: string;
itemReviewed?: string;
author?: Person;
/** Review status */
additionalType?: string;
isAnonymous?: boolean;
}

export default async function submitReview(
{
reviewBody,
reviewHeadline,
itemReviewed,
id,
author,
action,
additionalType,
isAnonymous,
}: Props,
_req: Request,
ctx: AppContext,
): Promise<Review | null> {
const isoDate = new Date().toISOString();
const records = await ctx.invoke.records.loaders.drizzle();

try {
if (action != "create") {
const storedReview = await getReviewById({ ctx, id });
if (!storedReview) {
return null;
}
const updateRecord = {
additionalType: additionalType ?? storedReview.additionalType,
reviewHeadline: reviewHeadline ?? storedReview.reviewHeadline,
reviewBody: reviewBody ?? storedReview.reviewBody,
dateModified: isoDate,
};
await records.update(review).set({
...updateRecord,
}).where(
eq(review.id, id!),
);

return {
...updateRecord,
"@type": "Review",
author: author ?? storedReview.author,
datePublished: storedReview.datePublished,
};
}

const insertData = {
itemReviewed,
isAnonymous,
author: author!,
additionalType: additionalType,
reviewHeadline: reviewHeadline,
reviewBody: reviewBody!,
datePublished: isoDate,
dateModified: isoDate,
};

await records.insert(review).values({
...insertData,
});

return {
"@type": "Review",
...insertData,
};
} catch (e) {
logger.error(e);
return null;
}
}
38 changes: 38 additions & 0 deletions blog/actions/submitView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { eq } from "https://esm.sh/v135/[email protected]";
import { postViews } from "../db/schema.ts";
import { AppContext } from "../mod.ts";
import { ViewFromDatabase } from "../types.ts";

export interface Props {
id: string;
}

export default async function action(
{ id }: Props,
_req: Request,
ctx: AppContext,
): Promise<{ count: number }> {
const records = await ctx.invoke.records.loaders.drizzle();

const existingRecord = await records.select()
.from(postViews)
.where(eq(postViews.id, id))
.get() as ViewFromDatabase | null;

if (!existingRecord) {
await records.insert(postViews).values({
id,
userInteractionCount: 1,
});

return { count: 1 };
}

const newCount = existingRecord.userInteractionCount! + 1;

await records.update(postViews)
.set({ userInteractionCount: newCount })
.where(eq(postViews.id, id));

return { count: newCount };
}
159 changes: 159 additions & 0 deletions blog/core/handlePosts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { postViews } from "../db/schema.ts";
import { AppContext } from "../mod.ts";
import { BlogPost, SortBy, ViewFromDatabase } from "../types.ts";
import { VALID_SORT_ORDERS } from "../utils/constants.ts";

/**
* Returns an sorted BlogPost list
*
* @param posts Posts to be sorted
* @param sortBy Sort option (must be: "date_desc" | "date_asc" | "title_asc" | "title_desc" | "view_asc" | "view_desc" )
*/
export const sortPosts = async (
blogPosts: BlogPost[],
sortBy: SortBy,
ctx: AppContext,
) => {
const splittedSort = sortBy.split("_");

if (splittedSort[0] === "view") {
//If sort is "view_asc" or "view_desc"

const records = await ctx.invoke.records.loaders.drizzle();
//Deco records not installed
if (records.__resolveType) {
throw new Error("Deco Records not installed!");
}

//Get views from database
const views = await records.select({
id: postViews.id,
userInteractionCount: postViews.userInteractionCount,
}).from(postViews) as ViewFromDatabase[] | null;

if (!views) {
return blogPosts;
}

//Act like a real extension
for (let i = 0; i < views.length; i++) {
const view = views[i];
const post = blogPosts.findIndex(({ slug }) => slug === view.id);

if (blogPosts[post]) {
blogPosts[post].interactionStatistic = {
"@type": "InteractionCounter",
userInteractionCount: view.userInteractionCount,
};
}
}

const sortOrder = VALID_SORT_ORDERS.includes(splittedSort[1])
? splittedSort[1]
: "desc";

//Sort and return
return blogPosts.toSorted((a, b) => {
const countOfA = a?.interactionStatistic?.userInteractionCount;
const countOfB = b?.interactionStatistic?.userInteractionCount;
if (
!countOfA &&
!countOfB
) {
return 0;
}

const comparison = (countOfA ?? 0) - (countOfB ?? 0);
return sortOrder === "desc" ? comparison : -comparison;
});
}

const sortMethod = splittedSort[0] in blogPosts[0]
? splittedSort[0] as keyof BlogPost
: "date";
const sortOrder = VALID_SORT_ORDERS.includes(splittedSort[1])
? splittedSort[1]
: "desc";

return blogPosts.toSorted((a, b) => {
if (!a[sortMethod] && !b[sortMethod]) {
return 0; // If both posts don't have the sort method, consider them equal
}
if (!a[sortMethod]) {
return 1; // If post a doesn't have sort method, put it after post b
}
if (!b[sortMethod]) {
return -1; // If post b doesn't have sort method, put it after post a
}
const comparison = sortMethod === "date"
? new Date(b.date).getTime() -
new Date(a.date).getTime()
: a[sortMethod]?.toString().localeCompare(
b[sortMethod]?.toString() ?? "",
) ?? 0;
return sortOrder === "desc" ? comparison : -comparison; // Invert sort depending of desc or asc
});
};

/**
* Returns an filtered BlogPost list
*
* @param posts Posts to be handled
* @param slug Category Slug to be filter
*/
export const filterPostsByCategory = (posts: BlogPost[], slug?: string) =>
slug
? posts.filter(({ categories }) => categories.find((c) => c.slug === slug))
: posts;

/**
* Returns an filtered BlogPost list by specific slugs
*
* @param posts Posts to be handled
* @param postSlugs Specific slugs to be filter
*/
export const filterPostsBySlugs = (posts: BlogPost[], postSlugs: string[]) =>
posts.filter(({ slug }) => postSlugs.includes(slug));

/**
* Returns an filtered and sorted BlogPost list
*
* @param posts Posts to be handled
* @param pageNumber Actual page number
* @param postsPerPage Number of posts per page
*/
export const slicePosts = (
posts: BlogPost[],
pageNumber: number,
postsPerPage: number,
) => {
const startIndex = (pageNumber - 1) * postsPerPage;
const endIndex = startIndex + postsPerPage;
return posts.slice(startIndex, endIndex);
};

/**
* Returns an filtered and sorted BlogPost list. It dont slice
*
* @param posts Posts to be handled
* @param sortBy Sort option (must be: "date_desc" | "date_asc" | "title_asc" | "title_desc")
* @param ctx AppContext
* @param slug Category slug to be filter
*/
export default async function handlePosts(
posts: BlogPost[],
sortBy: SortBy,
ctx: AppContext,
slug?: string,
postSlugs?: string[],
) {
const filteredPosts = postSlugs && postSlugs.length > 0
? filterPostsBySlugs(posts, postSlugs)
: filterPostsByCategory(posts, slug);

if (!filteredPosts || filteredPosts.length === 0) {
return null;
}

return await sortPosts(filteredPosts, sortBy, ctx);
}
Loading
Loading