diff --git a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/appender/AdminPersonalCoinPriceDataAppender.java b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/appender/AdminPersonalCoinPriceDataAppender.java new file mode 100644 index 0000000..57352f1 --- /dev/null +++ b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/appender/AdminPersonalCoinPriceDataAppender.java @@ -0,0 +1,114 @@ +package org.youngmonkeys.personal.admin.appender; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tvd12.ezyfox.bean.annotation.EzySingleton; +import org.youngmonkeys.ezyplatform.admin.appender.AdminDataAppender; +import org.youngmonkeys.ezyplatform.admin.service.AdminSettingService; +import org.youngmonkeys.ezyplatform.time.ClockProxy; +import org.youngmonkeys.personal.admin.repo.AdminPersonalCoinPriceRepository; +import org.youngmonkeys.personal.admin.service.AminPersonalCoinPriceService; +import org.youngmonkeys.personal.entity.PersonalCoinPrice; +import org.youngmonkeys.personal.result.CoinPriceApiResult; +import org.youngmonkeys.personal.service.PersonalCoinPriceService; + +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.youngmonkeys.personal.constant.PersonalConstants.COIN_SYMBOLS; + +@EzySingleton +public class AdminPersonalCoinPriceDataAppender + extends AdminDataAppender { + + private final ClockProxy clock; + private final AminPersonalCoinPriceService adminCoinPriceService; + private final PersonalCoinPriceService coinPriceService; + private final AdminPersonalCoinPriceRepository coinPriceRepository; + private long lastCallTime = System.currentTimeMillis(); + private static final int PERIOD = 60 * 1000; + + public AdminPersonalCoinPriceDataAppender( + ClockProxy clock, + ObjectMapper objectMapper, + AminPersonalCoinPriceService adminCoinPriceService, + PersonalCoinPriceService coinPriceService, + AdminSettingService settingService, + AdminPersonalCoinPriceRepository coinPriceRepository + ) { + super( + objectMapper, + settingService + ); + this.clock = clock; + this.adminCoinPriceService = adminCoinPriceService; + this.coinPriceService = coinPriceService; + this.coinPriceRepository = coinPriceRepository; + } + + @Override + protected List getValueList( + Long lastTimestamp + ) { + long callTime = lastCallTime + PERIOD; + long now = System.currentTimeMillis(); + if (callTime < now) { + lastCallTime = now; + return Arrays.asList(adminCoinPriceService.fetchCoinPrice()); + } + return Collections.emptyList(); + } + + @Override + protected PersonalCoinPrice toDataRecord(CoinPriceApiResult value) { + PersonalCoinPrice entity = new PersonalCoinPrice(); + entity.setSymbol(value.getBaseSymbol()); + entity.setName(value.getBaseName()); + entity.setPrice(value.getPrice()); + entity.setPriceChange(value.getPriceChange()); + entity.setUpdatedAt(clock.nowDateTime()); + + return entity; + } + + @Override + protected void addDataRecords(List dataRecords) { + List lastestResults = + new ArrayList<>(coinPriceService.getLatestPrices(COIN_SYMBOLS)); + List incomingResults = dataRecords.stream() + .map(coin -> { + CoinPriceApiResult result = new CoinPriceApiResult(); + result.setPrice(coin.getPrice()); + result.setBaseSymbol(coin.getSymbol()); + result.setBaseName(coin.getName()); + result.setPriceChange(coin.getPriceChange()); + return result; + }).collect(Collectors.toList()); + if (adminCoinPriceService.isAnyCoinChanged(lastestResults, incomingResults)) { + coinPriceRepository.save(dataRecords); + } + } + + @Override + protected Long extractNewLastPageToken(List list, Long aLong) { + return clock.nowDateTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + + @Override + protected String getAppenderNamePrefix() { + return "personal_coin_price"; + } + + @Override + protected Long defaultPageToken() { + return 0L; + } + + @Override + protected Class pageTokenType() { + return Long.class; + } +} diff --git a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/controller/api/AdminApiSettingController.java b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/controller/api/AdminApiSettingController.java index d6f46af..8935fb9 100644 --- a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/controller/api/AdminApiSettingController.java +++ b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/controller/api/AdminApiSettingController.java @@ -28,6 +28,11 @@ public ResponseEntity settingsPut( request.isAllowCalculatePostReadTime() ) ) + .registerOperation(() -> + personalSettingService.setShowCoinWidget( + request.isShowCoinWidget() + ) + ) .blockingExecute(); return ResponseEntity.noContent(); } diff --git a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/controller/view/AdminSettingsController.java b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/controller/view/AdminSettingsController.java index f179bd3..31bbbe7 100644 --- a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/controller/view/AdminSettingsController.java +++ b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/controller/view/AdminSettingsController.java @@ -30,6 +30,10 @@ public View settingsGet() { "allowCalculatePostReadTime", personalSettingService.isAllowCalculatePostReadTime() ) + .addVariable( + "showCoinWidget", + personalSettingService.isShowCoinWidget() + ) .build(); } } diff --git a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/event/AdminCoinPriceRequestEventHandler.java b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/event/AdminCoinPriceRequestEventHandler.java new file mode 100644 index 0000000..1e98244 --- /dev/null +++ b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/event/AdminCoinPriceRequestEventHandler.java @@ -0,0 +1,20 @@ +package org.youngmonkeys.personal.admin.event; + +import com.tvd12.ezyfox.bean.annotation.EzySingleton; +import lombok.AllArgsConstructor; +import org.youngmonkeys.ezyplatform.event.AbstractEventHandler; + +import java.util.Map; + +import static org.youngmonkeys.personal.constant.PersonalConstants.INTERNAL_EVENT_NAME_COIN_PRICE_UPDATE; + +@EzySingleton +@AllArgsConstructor +public class AdminCoinPriceRequestEventHandler + extends AbstractEventHandler, Void> { + + @Override + public String getEventName() { + return INTERNAL_EVENT_NAME_COIN_PRICE_UPDATE; + } +} diff --git a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/repo/AdminPersonalCoinPriceRepository.java b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/repo/AdminPersonalCoinPriceRepository.java new file mode 100644 index 0000000..25ff649 --- /dev/null +++ b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/repo/AdminPersonalCoinPriceRepository.java @@ -0,0 +1,10 @@ +package org.youngmonkeys.personal.admin.repo; + +import com.tvd12.ezydata.database.EzyDatabaseRepository; +import com.tvd12.ezyfox.database.annotation.EzyRepository; +import org.youngmonkeys.personal.entity.PersonalCoinPrice; +import org.youngmonkeys.personal.repo.PersonalCoinPriceRepository; + +@EzyRepository +public interface AdminPersonalCoinPriceRepository extends + PersonalCoinPriceRepository, EzyDatabaseRepository {} diff --git a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/request/AdminPersonalSaveSettingsRequest.java b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/request/AdminPersonalSaveSettingsRequest.java index 146bcf6..f16bae4 100644 --- a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/request/AdminPersonalSaveSettingsRequest.java +++ b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/request/AdminPersonalSaveSettingsRequest.java @@ -23,4 +23,5 @@ @Setter public class AdminPersonalSaveSettingsRequest { private boolean allowCalculatePostReadTime; + private boolean showCoinWidget; } diff --git a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/service/AdminPersonalSettingService.java b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/service/AdminPersonalSettingService.java index afa1286..751e2ea 100644 --- a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/service/AdminPersonalSettingService.java +++ b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/service/AdminPersonalSettingService.java @@ -5,6 +5,7 @@ import org.youngmonkeys.personal.service.PersonalSettingService; import static org.youngmonkeys.personal.constant.PersonalConstants.SETTING_KEY_ALLOW_CALCULATE_POST_READ_TIME; +import static org.youngmonkeys.personal.constant.PersonalConstants.SETTING_KEY_SHOW_COIN_PRICE; @Service public class AdminPersonalSettingService extends PersonalSettingService { @@ -24,4 +25,11 @@ public void setAllowCalculatePostReadTime(boolean allow) { allow ); } + + public void setShowCoinWidget(boolean allow) { + settingService.setBooleanValue( + SETTING_KEY_SHOW_COIN_PRICE, + allow + ); + } } diff --git a/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/service/AminPersonalCoinPriceService.java b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/service/AminPersonalCoinPriceService.java new file mode 100644 index 0000000..11948aa --- /dev/null +++ b/personal/personal-admin-plugin/src/main/java/org/youngmonkeys/personal/admin/service/AminPersonalCoinPriceService.java @@ -0,0 +1,70 @@ +package org.youngmonkeys.personal.admin.service; + +import com.tvd12.ezyfox.bean.annotation.EzySingleton; +import com.tvd12.ezyfox.util.EzyLoggable; +import com.tvd12.ezyhttp.client.HttpClient; +import com.tvd12.ezyhttp.client.request.GetRequest; +import com.tvd12.ezyhttp.client.request.Request; +import lombok.AllArgsConstructor; +import org.youngmonkeys.personal.result.CoinPriceApiResult; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.youngmonkeys.personal.constant.PersonalConstants.API_URL_COIN_PRICE; + +@EzySingleton +@AllArgsConstructor +public class AminPersonalCoinPriceService extends EzyLoggable { + + private final HttpClient httpClient; + + public CoinPriceApiResult[] fetchCoinPrice() { + try { + Request request = new GetRequest() + .setURL(API_URL_COIN_PRICE) + .setResponseType(200, CoinPriceApiResult[].class); + return httpClient.call(request); + } catch (Exception e) { + logger.warn("fetch coin price error", e); + } + return new CoinPriceApiResult[0]; + } + + private boolean isChanged( + CoinPriceApiResult oldValue, + CoinPriceApiResult newValue + ) { + if (oldValue == null || newValue == null) { + return true; + } + + return !Objects.equals(oldValue.getPrice(), newValue.getPrice()); + } + + public boolean isAnyCoinChanged( + List oldList, + List newList + ) { + if (oldList == null || newList == null) { + return true; + } + Map oldMap = + oldList.stream() + .collect(Collectors.toMap( + CoinPriceApiResult::getBaseSymbol, + Function.identity() + )); + for (CoinPriceApiResult newValue : newList) { + CoinPriceApiResult oldValue = oldMap.get(newValue.getBaseSymbol()); + + if (isChanged(oldValue, newValue)) { + return true; + } + } + return false; + } +} diff --git a/personal/personal-admin-plugin/src/main/resources/messages/messages_vi.properties b/personal/personal-admin-plugin/src/main/resources/messages/messages_vi.properties index 133da81..b960e1f 100644 --- a/personal/personal-admin-plugin/src/main/resources/messages/messages_vi.properties +++ b/personal/personal-admin-plugin/src/main/resources/messages/messages_vi.properties @@ -1,3 +1,4 @@ dashboard=B\u1EA3ng \u0111i\u1EC1u khi\u1EC3n dashboard_posts=C\u00E1c b\u00E0i vi\u1EBFt post_word_count_appender=Tr\u00ECnh \u0111\u1EBFm s\u1ED1 t\u1EEB c\u1EE7a b\u00E0i vi\u1EBFt +coin_price_appender=Tr\u00ECnh hi\u1EC3n th\u1ECB gi\u00E1 coin diff --git a/personal/personal-admin-plugin/src/main/resources/scripts/coin_price_table.sql b/personal/personal-admin-plugin/src/main/resources/scripts/coin_price_table.sql new file mode 100644 index 0000000..e9ae570 --- /dev/null +++ b/personal/personal-admin-plugin/src/main/resources/scripts/coin_price_table.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS `personal_coin_price` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `symbol` varchar(10) COLLATE utf8mb4_unicode_520_ci NOT NULL, + `name` varchar(50) COLLATE utf8mb4_unicode_520_ci NOT NULL, + `price` char(85) COLLATE utf8mb4_unicode_520_ci NOT NULL, + `price_change` char(85) COLLATE utf8mb4_unicode_520_ci, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; \ No newline at end of file diff --git a/personal/personal-admin-plugin/src/main/resources/templates/personal/settings.html b/personal/personal-admin-plugin/src/main/resources/templates/personal/settings.html index 3e15e4b..b4b17cd 100644 --- a/personal/personal-admin-plugin/src/main/resources/templates/personal/settings.html +++ b/personal/personal-admin-plugin/src/main/resources/templates/personal/settings.html @@ -16,6 +16,13 @@
+
+
+ [[#{coin_price_appender}]] +
+
+ +

@@ -31,6 +38,16 @@ [[#{invalid}]] +
+ +
+ +
+ +
@@ -55,6 +72,8 @@ var formData = ezyadmin.formDataToObject('updateSettingsForm'); formData.allowCalculatePostReadTime = $('#allowCalculatePostReadTime') .prop('checked'); + formData.showCoinWidget = $('#showCoinWidget') + .prop('checked'); $.ajax({ type: 'PUT', url: '/personal/api/v1/settings', diff --git a/personal/personal-admin-plugin/src/test/java/org/youngmonkeys/personal/admin/it/repo/AdminPersonalCoinPriceRepositoryIT.java b/personal/personal-admin-plugin/src/test/java/org/youngmonkeys/personal/admin/it/repo/AdminPersonalCoinPriceRepositoryIT.java new file mode 100644 index 0000000..8fb169a --- /dev/null +++ b/personal/personal-admin-plugin/src/test/java/org/youngmonkeys/personal/admin/it/repo/AdminPersonalCoinPriceRepositoryIT.java @@ -0,0 +1,35 @@ +package org.youngmonkeys.personal.admin.it.repo; + +import com.tvd12.ezyfox.bean.annotation.EzySingleton; +import com.tvd12.test.assertion.Asserts; +import lombok.AllArgsConstructor; +import org.youngmonkeys.devtools.InstanceRandom; +import org.youngmonkeys.ezyplatform.test.IntegrationTest; +import org.youngmonkeys.personal.admin.repo.AdminPersonalCoinPriceRepository; +import org.youngmonkeys.personal.entity.PersonalCoinPrice; + +@EzySingleton +@AllArgsConstructor +public class AdminPersonalCoinPriceRepositoryIT implements IntegrationTest { + + private final AdminPersonalCoinPriceRepository personalCoinPriceRepository; + + @Override + public void test() throws Exception { + saveFindTest(); + } + + private void saveFindTest() { + // given + PersonalCoinPrice entity = new InstanceRandom().randomObject(PersonalCoinPrice.class); + entity.setSymbol("test"); + + // when + personalCoinPriceRepository.save(entity); + PersonalCoinPrice actual = personalCoinPriceRepository.findById(entity.getId()); + + // then + Asserts.assertNotNull(actual); + personalCoinPriceRepository.delete(entity.getId()); + } +} diff --git a/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/constant/PersonalConstants.java b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/constant/PersonalConstants.java index a715c72..7be6283 100644 --- a/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/constant/PersonalConstants.java +++ b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/constant/PersonalConstants.java @@ -15,5 +15,19 @@ public final class PersonalConstants { public static final String SETTING_KEY_ALLOW_CALCULATE_POST_READ_TIME = "allow_calculate_post_read_time"; + public static final String TABLE_NAME_COIN_PRICE = + "personal_coin_price"; + + public static final String INTERNAL_EVENT_NAME_COIN_PRICE_UPDATE = + "personal_coin_price"; + + public static final String COIN_SYMBOLS = "BTC,ETH,BNB,LTC,XRP"; + + public static final String API_URL_COIN_PRICE = + "https://coinyep.com/api/v1/?list=" + COIN_SYMBOLS; + + public static final String SETTING_KEY_SHOW_COIN_PRICE = + "show_coin_widget"; + private PersonalConstants() {} } diff --git a/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/entity/PersonalCoinPrice.java b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/entity/PersonalCoinPrice.java new file mode 100644 index 0000000..adc8481 --- /dev/null +++ b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/entity/PersonalCoinPrice.java @@ -0,0 +1,33 @@ +package org.youngmonkeys.personal.entity; + +import lombok.*; + +import javax.persistence.*; +import java.time.LocalDateTime; + +import static org.youngmonkeys.personal.constant.PersonalConstants.TABLE_NAME_COIN_PRICE; + +@Getter +@Setter +@ToString +@Entity +@Table(name = TABLE_NAME_COIN_PRICE) +@AllArgsConstructor +@NoArgsConstructor +public class PersonalCoinPrice { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private String symbol; + + private String name; + + private String price; + + @Column(name = "price_change") + private String priceChange; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} diff --git a/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/repo/PersonalCoinPriceRepository.java b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/repo/PersonalCoinPriceRepository.java new file mode 100644 index 0000000..61b3e23 --- /dev/null +++ b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/repo/PersonalCoinPriceRepository.java @@ -0,0 +1,26 @@ +package org.youngmonkeys.personal.repo; + +import com.tvd12.ezydata.database.EzyDatabaseRepository; +import com.tvd12.ezyfox.database.annotation.EzyQuery; +import com.tvd12.ezyfox.database.annotation.EzyRepository; +import org.youngmonkeys.personal.entity.PersonalCoinPrice; +import org.youngmonkeys.personal.result.CoinPriceApiResult; + +import java.util.Collection; + +@EzyRepository +public interface PersonalCoinPriceRepository + extends EzyDatabaseRepository { + + @EzyQuery( + "SELECT p.symbol, p.name, p.price, p.priceChange " + + "FROM PersonalCoinPrice p " + + "WHERE p.symbol IN ?0 " + + "AND p.updatedAt = (" + + "SELECT MAX(p2.updatedAt) " + + "FROM PersonalCoinPrice p2 " + + "WHERE p2.symbol = p.symbol" + + ")" + ) + Collection findLatestBySymbols(Collection symbols); +} diff --git a/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/result/CoinPriceApiResult.java b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/result/CoinPriceApiResult.java new file mode 100644 index 0000000..ab3dc08 --- /dev/null +++ b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/result/CoinPriceApiResult.java @@ -0,0 +1,23 @@ +package org.youngmonkeys.personal.result; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.tvd12.ezyfox.database.annotation.EzyQueryResult; +import lombok.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@EzyQueryResult +public class CoinPriceApiResult { + + @JsonProperty("base_symbol") + private String baseSymbol; + + @JsonProperty("base_name") + private String baseName; + + private String price; + + @JsonProperty("price_change") + private String priceChange; +} diff --git a/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/service/PersonalCoinPriceService.java b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/service/PersonalCoinPriceService.java new file mode 100644 index 0000000..dbf3d63 --- /dev/null +++ b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/service/PersonalCoinPriceService.java @@ -0,0 +1,20 @@ +package org.youngmonkeys.personal.service; + +import com.tvd12.ezyfox.bean.annotation.EzySingleton; +import lombok.AllArgsConstructor; +import org.youngmonkeys.personal.repo.PersonalCoinPriceRepository; +import org.youngmonkeys.personal.result.CoinPriceApiResult; + +import java.util.Arrays; +import java.util.Collection; + +@EzySingleton +@AllArgsConstructor +public class PersonalCoinPriceService { + private final PersonalCoinPriceRepository coinPriceRepository; + + public Collection getLatestPrices(String symbols) { + Collection symbolsList = Arrays.asList(symbols.split(",")); + return coinPriceRepository.findLatestBySymbols(symbolsList); + } +} diff --git a/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/service/PersonalSettingService.java b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/service/PersonalSettingService.java index 7153b7a..3e5ae16 100644 --- a/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/service/PersonalSettingService.java +++ b/personal/personal-sdk/src/main/java/org/youngmonkeys/personal/service/PersonalSettingService.java @@ -4,6 +4,7 @@ import org.youngmonkeys.ezyplatform.service.SettingService; import static org.youngmonkeys.personal.constant.PersonalConstants.SETTING_KEY_ALLOW_CALCULATE_POST_READ_TIME; +import static org.youngmonkeys.personal.constant.PersonalConstants.SETTING_KEY_SHOW_COIN_PRICE; @AllArgsConstructor public class PersonalSettingService { @@ -16,4 +17,11 @@ public boolean isAllowCalculatePostReadTime() { Boolean.TRUE ); } + + public boolean isShowCoinWidget() { + return settingService.getBooleanValue( + SETTING_KEY_SHOW_COIN_PRICE, + Boolean.TRUE + ); + } } diff --git a/personal/personal-theme/src/main/resources/messages/messages_vi.properties b/personal/personal-theme/src/main/resources/messages/messages_vi.properties index 98325f2..3f92971 100644 --- a/personal/personal-theme/src/main/resources/messages/messages_vi.properties +++ b/personal/personal-theme/src/main/resources/messages/messages_vi.properties @@ -25,6 +25,9 @@ newer_posts=B\u00E0i vi\u1EBFt m\u1EDBi h\u01A1n older_posts=B\u00E0i vi\u1EBFt c\u0169 h\u01A1n post_at=\u0110\u0103ng l\u00FAc posted_by=\u0110\u0103ng b\u1EDFi +price=Gi\u00E1 +price_coin=Gi\u00E1 Coin +price_change=Bi\u1EBFn \u0111\u1ED9ng privacy_policy=Ch\u00EDnh s\u00E1ch b\u1EA3o m\u1EADt quick_links=Li\u00EAn k\u1EBFt nhanh subscribe=\u0110\u0103ng k\u00FD diff --git a/personal/personal-theme/src/main/resources/static/css/coin-price.css b/personal/personal-theme/src/main/resources/static/css/coin-price.css new file mode 100644 index 0000000..4bf8eab --- /dev/null +++ b/personal/personal-theme/src/main/resources/static/css/coin-price.css @@ -0,0 +1,109 @@ +.coin-widget { + position: fixed; + right: 20px; + bottom: 80px; + z-index: 9999; +} + +/* Toggle button */ +.coin-widget-toggle { + width: 48px; + height: 48px; + background: #667FEA; + color: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); +} + +/* Panel */ +.coin-widget-panel { + position: absolute; + right: 0; + bottom: 60px; + width: 360px; + background: #fff; + border-radius: 8px; + box-shadow: 0 8px 30px rgba(0,0,0,0.2); + overflow: hidden; + display: none; +} + +/* Expanded */ +.coin-widget.expanded .coin-widget-panel { + display: block; +} + +/* Header */ +.coin-widget-header { + display: flex; + justify-content: space-between; + padding: 10px 12px; + background: #667FEA; + color: #fff; + font-weight: 800; +} + +.close-btn { + color: #fff; +} + +.coin-widget-body { + max-height: 360px; + overflow-y: auto; +} + +.coin-table { + width: 100%; + border-collapse: collapse; +} + +.coin-table th, +.coin-table td { + padding: 8px; + text-align: right; + border-bottom: 1px solid #eee; +} + +.coin-table th:nth-child(1), +.coin-table td:nth-child(1) { + text-align: left; +} + +.coin-info .symbol { + font-size: 1.1rem; + font-weight: bold; +} + +.coin-info .name { + font-size: 0.85rem; + color: #888; +} + +.coin-price { + font-size: 1.1rem; + font-weight: 600; + color: #333; +} + +.price-up { + color: #16a34a; /* green */ + font-weight: 600; +} + +.price-down { + color: #dc2626; /* red */ + font-weight: 600; +} + +.price-flat { + color: #6b7280; /* gray */ +} + +.price-arrow { + margin-right: 4px; + font-size: 12px; +} \ No newline at end of file diff --git a/personal/personal-theme/src/main/resources/static/js/coin-price.js b/personal/personal-theme/src/main/resources/static/js/coin-price.js new file mode 100644 index 0000000..e1bc32c --- /dev/null +++ b/personal/personal-theme/src/main/resources/static/js/coin-price.js @@ -0,0 +1,66 @@ +$(function () { + const widget = $('#coin-widget'); + + $('#coin-widget-toggle').on('click', function () { + widget.toggleClass('expanded'); + }); + + $('#coin-widget-close').on('click', function () { + widget.removeClass('expanded'); + }); + + loadCoinPrices(); +}); + +function renderPriceChange(priceChange) { + if (!priceChange) { + return ''; + } + + if (priceChange.startsWith('-')) { + return ` + + ${priceChange} + + `; + } + + if (priceChange === '0' || priceChange === '0.00') { + return ` + + 0 + + `; + } + + return ` + + ${priceChange} + + `; +} + +function loadCoinPrices() { + $.get('/api/v1/coins/latest', function (data) { + console.log(data); + const tbody = $('#coin-price-body'); + tbody.empty(); + + data.forEach(function (coin) { + const changeClass = coin.price_change >= 0 ? 'text-success' : 'text-danger'; + + tbody.append(` + + + ${coin.base_symbol} + ${coin.base_name} + + ${coin.price} + + ${renderPriceChange(coin.price_change)}% + + + `); + }); + }); +} diff --git a/personal/personal-theme/src/main/resources/templates/fragments/coin-price-widget.html b/personal/personal-theme/src/main/resources/templates/fragments/coin-price-widget.html new file mode 100644 index 0000000..3172c59 --- /dev/null +++ b/personal/personal-theme/src/main/resources/templates/fragments/coin-price-widget.html @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/personal/personal-theme/src/main/resources/templates/page.html b/personal/personal-theme/src/main/resources/templates/page.html index d9f3c28..aeb6fe8 100644 --- a/personal/personal-theme/src/main/resources/templates/page.html +++ b/personal/personal-theme/src/main/resources/templates/page.html @@ -16,6 +16,7 @@ + @@ -67,6 +68,7 @@ + @@ -154,5 +156,8 @@ + + + diff --git a/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/controller/api/PersonalWebApiCoinPriceController.java b/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/controller/api/PersonalWebApiCoinPriceController.java new file mode 100644 index 0000000..60be888 --- /dev/null +++ b/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/controller/api/PersonalWebApiCoinPriceController.java @@ -0,0 +1,23 @@ +package org.youngmonkeys.personal.web.controller.api; + +import com.tvd12.ezyhttp.server.core.annotation.Controller; +import com.tvd12.ezyhttp.server.core.annotation.DoGet; +import lombok.AllArgsConstructor; +import org.youngmonkeys.personal.result.CoinPriceApiResult; +import org.youngmonkeys.personal.service.PersonalCoinPriceService; + +import java.util.Collection; + +import static org.youngmonkeys.personal.constant.PersonalConstants.COIN_SYMBOLS; + +@Controller("/api/v1") +@AllArgsConstructor +public class PersonalWebApiCoinPriceController { + + private final PersonalCoinPriceService coinPriceService; + + @DoGet("/coins/latest") + public Collection getLastestCoinPrices() { + return coinPriceService.getLatestPrices(COIN_SYMBOLS); + } +} diff --git a/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/controller/view/PersonalHomeController.java b/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/controller/view/PersonalHomeController.java index 7a3fda2..5a07aca 100644 --- a/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/controller/view/PersonalHomeController.java +++ b/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/controller/view/PersonalHomeController.java @@ -25,6 +25,7 @@ import static org.youngmonkeys.ezyplatform.util.StringConverters.trimOrNull; import static org.youngmonkeys.ezysupport.constant.EzySupportConstants.SETTING_NAME_BANNER_IMAGE_URL; +import static org.youngmonkeys.personal.constant.PersonalConstants.SETTING_KEY_SHOW_COIN_PRICE; @Setter public class PersonalHomeController { @@ -109,6 +110,10 @@ public View home( ) ) .addVariable("pageTitle", "home") + .addVariable( + "showCoinWidget", + settingService.getBooleanValue(SETTING_KEY_SHOW_COIN_PRICE) + ) .build(); blogsViewDecorator.decorateBlogView(view); return view; diff --git a/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/decorator/WebTopBlogDecorator.java b/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/decorator/WebTopBlogDecorator.java new file mode 100644 index 0000000..1930446 --- /dev/null +++ b/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/decorator/WebTopBlogDecorator.java @@ -0,0 +1,85 @@ +package org.youngmonkeys.personal.web.decorator; + +import com.tvd12.ezyfox.bean.annotation.EzySingleton; +import org.youngmonkeys.ezyarticle.sdk.model.PostI18nModel; +import org.youngmonkeys.ezyarticle.sdk.model.PostModel; +import org.youngmonkeys.ezyarticle.web.service.WebPostI18nService; +import org.youngmonkeys.ezyarticle.web.service.WebPostSlugService; +import org.youngmonkeys.ezyplatform.model.MediaNameModel; +import org.youngmonkeys.ezyplatform.web.service.WebMediaService; +import org.youngmonkeys.personal.web.response.TopBlogResponse; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@EzySingleton +public class WebTopBlogDecorator { + private final WebMediaService mediaService; + private final WebPostSlugService postSlugService; + private final WebPostI18nService postI18nService; + + public WebTopBlogDecorator( + WebMediaService mediaService, + WebPostSlugService postSlugService, + WebPostI18nService postI18nService + ) { + this.mediaService = mediaService; + this.postSlugService = postSlugService; + this.postI18nService = postI18nService; + } + + public List decorate( + List posts, + Map viewsMap, + String language + ) { + if (posts.isEmpty()) { + return Collections.emptyList(); + } + + List postIds = posts.stream() + .map(PostModel::getId) + .collect(Collectors.toList()); + + Set mediaIds = posts.stream() + .map(PostModel::getFeaturedImageId) + .filter(id -> id > 0) + .collect(Collectors.toSet()); + + Map i18nMap = + postI18nService.getShortedPostI18nMapByPostIdsAndLanguage( + postIds, language + ); + + Map slugMap = + postSlugService.getLatestSlugMapByPostIds(postIds); + + Map mediaMap = + mediaService.getMediaNameMapByIds(mediaIds); + + return posts.stream() + .map(post -> { + PostI18nModel i18n = i18nMap.get(post.getId()); + MediaNameModel media = mediaMap.get(post.getFeaturedImageId()); + + String title = + i18n != null ? i18n.getTitle() : post.getTitle(); + + String imageUrl = + MediaNameModel.getMediaUrlOrNull(media); + + return TopBlogResponse.builder() + .id(post.getId()) + .title(title) + .slug(slugMap.get(post.getId())) + .featuredImageUrl(imageUrl) + .views(viewsMap.get(post.getId())) + .publishedAt(post.getPublishedAtDateTime()) + .build(); + }) + .collect(Collectors.toList()); + } +} diff --git a/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/repo/WebPersonalCoinPriceRepository.java b/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/repo/WebPersonalCoinPriceRepository.java new file mode 100644 index 0000000..631cf07 --- /dev/null +++ b/personal/personal-web-plugin/src/main/java/org/youngmonkeys/personal/web/repo/WebPersonalCoinPriceRepository.java @@ -0,0 +1,6 @@ +package org.youngmonkeys.personal.web.repo; + +import org.youngmonkeys.personal.repo.PersonalCoinPriceRepository; + +public interface WebPersonalCoinPriceRepository + extends PersonalCoinPriceRepository { } diff --git a/personal/pom.xml b/personal/pom.xml index fb56a85..ffe92b8 100644 --- a/personal/pom.xml +++ b/personal/pom.xml @@ -18,8 +18,8 @@ ${env.EZYPLATFORM_HOME} 1.18.3 - 0.9.1 - 0.1.4 + 0.9.2 + 0.1.5 1.6.2 0.3.0 1.1.0