Skip to content

Commit

Permalink
Finalize blog post rating
Browse files Browse the repository at this point in the history
  • Loading branch information
gdevxy committed Dec 29, 2024
1 parent ba6f861 commit 8a076bc
Show file tree
Hide file tree
Showing 17 changed files with 211 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

import com.gdevxy.blog.client.google.model.CaptchaVerifyRequest;
import com.gdevxy.blog.client.google.model.CaptchaVerifyResponse;
import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
Expand All @@ -13,6 +13,6 @@ public interface GoogleClient {

@POST
@Path("/recaptcha/api/siteverify")
Uni<CaptchaVerifyResponse> verifyCaptcha(CaptchaVerifyRequest req);
Uni<CaptchaVerifyResponse> verifyCaptcha(@QueryParam("secret") String secret, @QueryParam("response") String response);

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.gdevxy.blog.component;

import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import jakarta.validation.Valid;
Expand All @@ -12,7 +13,9 @@
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import com.gdevxy.blog.component.cookie.Cookies;
import com.gdevxy.blog.model.BlogPost;
import com.gdevxy.blog.model.BlogPostRateReq;
import com.gdevxy.blog.model.Image;
Expand All @@ -23,6 +26,7 @@
import io.quarkus.qute.TemplateInstance;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.vertx.core.http.HttpServerRequest;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
Expand All @@ -37,7 +41,8 @@ public class BlogPostResource {
@GET
@Path("/{slug}")
@Produces(MediaType.TEXT_HTML)
public Uni<TemplateInstance> blogPost(@Valid @Size(max = 255) @PathParam("slug") String slug, @QueryParam("previewToken") String previewToken) {
public Uni<TemplateInstance> blogPost(HttpServerRequest req, @Valid @Size(max = 255) @PathParam("slug") String slug,
@QueryParam("previewToken") String previewToken) {

var blogPost = blogPostService.findBlogPost(previewToken, slug);
var images = blogPost.onItem().transformToMulti(p -> Multi.createFrom().iterable(p.getBlocks()))
Expand All @@ -47,22 +52,36 @@ public Uni<TemplateInstance> blogPost(@Valid @Size(max = 255) @PathParam("slug")
.collect()
.with(Collectors.toUnmodifiableMap(Image::getId, i -> i));

return Uni.combine().all().unis(blogPost, images)
var thumbsUpEnabled = blogPost.map(p -> Cookies.findBlogPostRatingCookie(req, p.getId())).map(Optional::isPresent);

return Uni.combine().all().unis(blogPost, images, thumbsUpEnabled)
.with(Templates::blogPost);
}

@POST
@Path("/{id}/rate")
public Uni<Void> rate(@Valid @Size(max = 22) @PathParam("id") String id, @Valid BlogPostRateReq req) {
@Path("/{id}/thumbs-up")
public Uni<Response> thumbsUp(@Valid @Size(max = 22) @PathParam("id") String key, @Valid BlogPostRateReq req) {

return blogPostService.thumbsUp(key, req.captcha())
.map(uuid -> Response.noContent()
.cookie(Cookies.createBlogPostRatingCookie(key, uuid))
.build());
}

@POST
@Path("/{id}/thumbs-down")
public Uni<Response> thumbsDown(HttpServerRequest req, @Valid @Size(max = 22) @PathParam("id") String key) {

return blogPostService.rate(id, req.captcha());
return Cookies.findBlogPostRatingCookie(req, key)
.map(c -> blogPostService.thumbsDown(c.getValue()).map(v -> Response.ok().cookie(Cookies.deleteBlogPostRatingCookie(key)).build()))
.orElseGet(() -> Uni.createFrom().item(Response.noContent().build()));
}

@CheckedTemplate
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Templates {

public static native TemplateInstance blogPost(BlogPost blogPost, Map<String, Image> images);
public static native TemplateInstance blogPost(BlogPost blogPost, Map<String, Image> images, Boolean thumbsUpEnabled);

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.gdevxy.blog.component.cookie;

import java.time.Instant;
import java.util.Date;
import java.util.Optional;

import jakarta.ws.rs.core.NewCookie;

import io.vertx.core.http.Cookie;
import io.vertx.core.http.HttpServerRequest;
import lombok.experimental.UtilityClass;

@UtilityClass
public class Cookies {

private final String BLOG_POST_RATING_COOKIE_NAME = "gdevxy-bpr-%s";

public Optional<Cookie> findBlogPostRatingCookie(HttpServerRequest req, String id) {

return req.cookies(BLOG_POST_RATING_COOKIE_NAME.formatted(id)).stream().findAny();
}

public NewCookie createBlogPostRatingCookie(String id, String uuid) {

return new NewCookie.Builder(BLOG_POST_RATING_COOKIE_NAME.formatted(id))
.secure(true)
.httpOnly(true)
.sameSite(NewCookie.SameSite.STRICT)
.maxAge(Integer.MAX_VALUE)
.expiry(Date.from(Instant.now().plusSeconds(Integer.MAX_VALUE)))
.value(uuid)
.path("/")
.build();
}

public NewCookie deleteBlogPostRatingCookie(String id) {

return new NewCookie.Builder(BLOG_POST_RATING_COOKIE_NAME.formatted(id))
.secure(true)
.httpOnly(true)
.sameSite(NewCookie.SameSite.STRICT)
.maxAge(Integer.MAX_VALUE)
.expiry(Date.from(Instant.now().plusSeconds(Integer.MAX_VALUE)))
.value("goodbye")
.path("/")
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{/extraCss}

{#extraJs}
<script src="https://www.google.com/recaptcha/api.js?render=6LeWR6gqAAAAAC4uiRPFjYaOaVLrTXh4m94Wbgs0"></script>
<script src="https://www.google.com/recaptcha/api.js?render=6Lc7vagqAAAAAKi_E_E275yxYo_B80-RvOVmVaid"></script>
<script src="/webjars/highlightjs/highlight.min.js"></script>
<script src="/webjars/highlightjs/languages/java.min.js"></script>
<script src="/webjars/highlightjs/languages/json.min.js"></script>
Expand All @@ -21,28 +21,45 @@
<script src="/_static/jquery/dist/jquery.min.js"></script>
<script>
$(document).ready(function () {
$("#btnRate").on('click', function () {
grecaptcha.ready(function () {
grecaptcha
.execute("6LeWR6gqAAAAAC4uiRPFjYaOaVLrTXh4m94Wbgs0", { action: "rate_post" })
.then(function (token) {
$.ajax({
type: 'POST',
url: `/blog-posts/{blogPost.id}/rate`,
dataType: 'json',
contentType: "application/json",
data: JSON.stringify({
captcha: token
}),
success: function () {
$(`#btnRate`).addClass("text-neon").addClass("pulse");
},
error: function (err) {
$(`#btnRate`).addClass("tada");
}
$("#btnRate").on('click', function (e) {

if($(e.target).hasClass("thumbs-up-success")) {

$.ajax({
type: 'POST',
url: `/blog-posts/{blogPost.id}/thumbs-down`,
success: function () {
$(`#btnRate`).removeClass("thumbs-up-success").removeClass("pulse");
},
error: function (err) {
$(`#btnRate`).addClass("tada");
}
});

} else {

grecaptcha.ready(function () {
grecaptcha
.execute("6Lc7vagqAAAAAKi_E_E275yxYo_B80-RvOVmVaid", { action: "submit" })
.then(function (token) {
$.ajax({
type: 'POST',
url: `/blog-posts/{blogPost.id}/thumbs-up`,
dataType: 'json',
contentType: "application/json",
data: JSON.stringify({
captcha: token
}),
success: function () {
$(`#btnRate`).addClass("thumbs-up-success").addClass("pulse");
},
error: function (err) {
$(`#btnRate`).addClass("tada");
}
});
});
});
});
});
}
});
});
</script>
Expand Down Expand Up @@ -125,7 +142,7 @@ <h1 class="card-title">{blogPost.title}</h1>
<div class="col text-end">
{/if}
<div class="blog-paragraph">
Leave a thumbs up if you liked this post <span class="ps-2"><button id="btnRate" type="button" class="btn btn-dark bi bi-hand-thumbs-up-fill"></button></span>
Leave a thumbs up if you liked it <span class="ps-2"><button id="btnRate" type="button" class="btn btn-dark bi bi-hand-thumbs-up-fill {#if thumbsUpEnabled}thumbs-up-success{/}"></button></span>
</div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions component/src/main/resources/web/app/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
padding-left: 3em;
}

.text-neon {
.text-neon, .thumbs-up-success {
color: var(--bs-color-neon);
}

Expand Down Expand Up @@ -171,7 +171,7 @@
}

.pulse {
animation: pulse-animation 2s infinite;
animation: pulse-animation 2s 1;
}

@keyframes pulse-animation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import io.vertx.mutiny.sqlclient.Row;
import io.vertx.mutiny.sqlclient.Tuple;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@ApplicationScoped
@RequiredArgsConstructor
public class BlogPostDao extends DaoSupport {
Expand All @@ -20,26 +22,32 @@ public Uni<BlogPostEntity> findByKey(String key) {
return as(sql.preparedQuery("""
select
id,
key,
rating
from blog_post where key = $1
key
from blog_post
where key = $1
""")
.execute(Tuple.of(key)), BlogPostDao::toBlogPostEntity);
}

public Uni<Void> rate(String key) {
public Uni<BlogPostEntity> save(BlogPostEntity entity) {

return sql.preparedQuery("""
update blog_post set rating = coalesce(rating, 0) + 1 where key = $1
""").execute(Tuple.of(key)).replaceWithVoid();
log.info("Persisting: {}", entity);

return as(sql.preparedQuery("""
insert into blog_post(
key
) values (
$1
) returning *
""")
.execute(Tuple.of(entity.getKey())), row -> entity.toBuilder().id(row.getInteger("id")).build());
}

private static BlogPostEntity toBlogPostEntity(Row row) {

return BlogPostEntity.builder()
.id(row.getInteger("id"))
.key(row.getString("key"))
.rating(row.getInteger("rating"))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
import lombok.ToString;

@Getter
@Builder
@Builder(toBuilder = true)
@ToString
public class BlogPostEntity {

private final Integer id;
private final String key;
private final Integer rating;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.gdevxy.blog.dao.blogpost;

import java.util.UUID;

import jakarta.enterprise.context.ApplicationScoped;

import com.gdevxy.blog.dao.DaoSupport;
import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.pgclient.PgPool;
import io.vertx.mutiny.sqlclient.Tuple;
import lombok.RequiredArgsConstructor;

@ApplicationScoped
@RequiredArgsConstructor
public class BlogPostRatingDao extends DaoSupport {

private final PgPool sql;

public Uni<Long> findRating(Integer id) {

return as(sql.preparedQuery("""
select
count(*) as rating
from blog_post_rating
where blog_post_id = $1
""")
.execute(Tuple.of(id)), r -> r.getLong("rating"));
}

public Uni<String> thumbsUp(Integer blogId) {

var uuid = UUID.randomUUID().toString();

return sql.preparedQuery("""
insert into blog_post_rating (blog_post_id, uuid) values ($1, $2)
""").execute(Tuple.of(blogId, uuid)).map(i -> uuid);
}

public Uni<Void> thumbsDown(String uuid) {

return sql.preparedQuery("""
delete from blog_post_rating where uuid = $1
""").execute(Tuple.of(uuid)).replaceWithVoid();
}

}

This file was deleted.

This file was deleted.

Loading

0 comments on commit 8a076bc

Please sign in to comment.