Skip to content

Commit

Permalink
feat: add config options and load items from hc api
Browse files Browse the repository at this point in the history
  • Loading branch information
taciturnaxolotl committed Sep 14, 2024
1 parent f7cf28c commit 820d218
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 23 deletions.
6 changes: 6 additions & 0 deletions config.default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,9 @@ mail:
username:
password:
tls:

shop:
enabled: true
airtable_api_key:
airtable_base_id:
airtable_product_table_name:
8 changes: 8 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,13 @@ type SMTPMailConfig struct {
SkipVerify bool `env:"WAKAPI_MAIL_SMTP_SKIP_VERIFY"`
}

type shopConfig struct {
Enabled bool `yaml:"enabled" default:"false" env:"WAKAPI_SHOP_ENABLED"`
AirtableApiKey string `env:"WAKAPI_SHOP_AIRTABLE_API_KEY"`
AirtableBaseId string `env:"WAKAPI_SHOP_AIRTABLE_BASE_ID"`
AirtableProductTableName string `env:"WAKAPI_SHOP_AIRTABLE_PRODUCT_TABLE_NAME"`
}

type Config struct {
Env string `default:"dev" env:"ENVIRONMENT"`
Version string `yaml:"-"`
Expand All @@ -203,6 +210,7 @@ type Config struct {
Subscriptions subscriptionsConfig
Sentry sentryConfig
Mail mailConfig
Shop shopConfig
}

func (c *Config) CreateCookie(name, value string) *http.Cookie {
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ require (
gorm.io/gorm v1.25.11
)

require github.com/cespare/xxhash/v2 v2.3.0 // indirect
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/fabioberger/airtable-go v3.1.0+incompatible // indirect
)

require (
filippo.io/edwards25519 v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpz
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY=
github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/fabioberger/airtable-go v3.1.0+incompatible h1:n5dw+HWBc+hytrVL75xe94EGt7FtNFGDII1tNoWTCAE=
github.com/fabioberger/airtable-go v3.1.0+incompatible/go.mod h1:EoKuSh7EefzhMCyVr6iXPlgFzDgHyZCZ3E5Sg8Cy9GM=
github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k=
github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
Expand Down
4 changes: 3 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ var (
diagnosticsService services.IDiagnosticsService
housekeepingService services.IHousekeepingService
miscService services.IMiscService
shopService services.IShopService
)

// TODO: Refactor entire project to be structured after business domains
Expand Down Expand Up @@ -188,6 +189,7 @@ func main() {
diagnosticsService = services.NewDiagnosticsService(diagnosticsRepository)
housekeepingService = services.NewHousekeepingService(userService, heartbeatService, summaryService)
miscService = services.NewMiscService(userService, heartbeatService, summaryService, keyValueService, mailService)
shopService = services.NewShopService()

if config.App.LeaderboardEnabled {
leaderboardService = services.NewLeaderboardService(leaderboardRepository, summaryService, userService)
Expand Down Expand Up @@ -233,7 +235,7 @@ func main() {
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, projectLabelService, keyValueService, mailService)
subscriptionHandler := routes.NewSubscriptionHandler(userService, mailService, keyValueService)
projectsHandler := routes.NewProjectsHandler(userService, heartbeatService)
shopHandler := routes.NewShopHandler(userService)
shopHandler := routes.NewShopHandler(userService, shopService)
homeHandler := routes.NewHomeHandler(userService, keyValueService)
loginHandler := routes.NewLoginHandler(userService, mailService, keyValueService)
imprintHandler := routes.NewImprintHandler(keyValueService)
Expand Down
1 change: 1 addition & 0 deletions models/products.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type Product struct {
ID uint `json:"id" gorm:"primary_key"`
Name string `json:"name"`
Price int `json:"price"`
Stock int `json:"stock"`
Description string `json:"description"`
Image string `json:"image"`
}
Expand Down
16 changes: 7 additions & 9 deletions routes/shop.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"github.com/go-chi/chi/v5"
conf "github.com/kcoderhtml/hackatime/config"
"github.com/kcoderhtml/hackatime/middlewares"
"github.com/kcoderhtml/hackatime/models"
"github.com/kcoderhtml/hackatime/models/view"
routeutils "github.com/kcoderhtml/hackatime/routes/utils"
"github.com/kcoderhtml/hackatime/services"
Expand All @@ -16,12 +15,14 @@ import (
type ShopHandler struct {
config *conf.Config
userService services.IUserService
shopService services.IShopService
}

func NewShopHandler(userService services.IUserService) *ShopHandler {
func NewShopHandler(userService services.IUserService, shopService services.IShopService) *ShopHandler {
return &ShopHandler{
config: conf.Get(),
userService: userService,
shopService: shopService,
}
}

Expand Down Expand Up @@ -54,13 +55,10 @@ func (h *ShopHandler) buildViewModel(r *http.Request, w http.ResponseWriter) *vi
return h.buildViewModel(r, w).WithError("unauthorized")
}

products := []*models.Product{
{
Name: "Sticker Pile",
Price: 1,
Description: "We'll send you 3 random stickers! (Available anywhere!)",
Image: "https://cloud-c1gqq7ttf-hack-club-bot.vercel.app/0sticker_pile_2.png",
},
products, err := h.shopService.GetProducts()
if err != nil {
conf.Log().Request(r).Error("failed to get products", "error", err.Error())
return h.buildViewModel(r, w).WithError("failed to get products")
}

pageParams := utils.ParsePageParamsWithDefault(r, 1, 24)
Expand Down
4 changes: 4 additions & 0 deletions services/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,7 @@ type IUserService interface {
FlushCache()
FlushUserCache(string)
}

type IShopService interface {
GetProducts() ([]*models.Product, error)
}
90 changes: 90 additions & 0 deletions services/shop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package services

import (
"encoding/json"
"fmt"
"net/http"
"sort"
"time"

"github.com/kcoderhtml/hackatime/config"
"github.com/kcoderhtml/hackatime/models"
"github.com/patrickmn/go-cache"
)

type ShopService struct {
config *config.Config
cache *cache.Cache
}

func NewShopService() *ShopService {
return &ShopService{
config: config.Get(),
cache: cache.New(6*time.Hour, 6*time.Hour),
}
}

type HackClubProduct struct {
Name string `json:"name"`
SmallName string `json:"smallName"`
Description string `json:"description"`
Hours int `json:"hours"`
ImageURL string `json:"imageURL"`
Stock *int `json:"stock"` // Change to pointer to allow null
}

func (srv *ShopService) GetProducts() ([]*models.Product, error) {
// Check if products are in cache
if cachedProducts, found := srv.cache.Get("products"); found {
return cachedProducts.([]*models.Product), nil
}

// Fetch products from Hack Club API
resp, err := http.Get("https://hackclub.com/api/arcade/shop/")
if err != nil {
return nil, fmt.Errorf("error fetching products: %v", err)
}
defer resp.Body.Close()

var hackClubProducts []HackClubProduct
if err := json.NewDecoder(resp.Body).Decode(&hackClubProducts); err != nil {
return nil, fmt.Errorf("error decoding products: %v", err)
}

formattedProducts := []*models.Product{}

for i, product := range hackClubProducts {
description := product.Description
if description == "" {
description = product.SmallName
}

stock := -1
if product.Stock != nil {
stock = *product.Stock
}

formattedProducts = append(formattedProducts, &models.Product{
ID: uint(i + 1), // Use index + 1 as ID
Name: product.Name,
Description: description,
Price: product.Hours,
Stock: stock,
Image: product.ImageURL,
})
}

// Sort products by price
sort.Slice(formattedProducts, func(i, j int) bool {
return formattedProducts[i].Price < formattedProducts[j].Price
})

// Cache the sorted formatted products
srv.cache.Set("products", formattedProducts, cache.DefaultExpiration)

return formattedProducts, nil
}

func (srv *ShopService) ClearProductsCache() {
srv.cache.Delete("products")
}
2 changes: 1 addition & 1 deletion static/assets/css/app.dist.v0.1.6.css

Large diffs are not rendered by default.

Binary file modified static/assets/css/app.dist.v0.1.6.css.br
Binary file not shown.
60 changes: 49 additions & 11 deletions views/shop.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@
#logo-side {
left: 0;
}

.stock-badge {
position: absolute;
top: 0;
left: 0;
transform: rotate(-2deg);
padding: 5px 30px;
font-size: 0.8rem;
font-weight: bold;
color: white;
z-index: 10;
}
</style>

<main class="mt-10 grow flex justify-center w-full">
Expand All @@ -37,33 +49,59 @@
>
{{ range .Products }}
<div
class="product bg-accent-primary dark:bg-accent-dark-primary rounded-lg shadow-md overflow-hidden"
class="product bg-accent-primary dark:bg-accent-dark-primary rounded-lg shadow-md overflow-hidden relative {{ if eq .Stock 0 }}opacity-50{{ end }} flex flex-col h-full"
>
<img
src="{{ .Image }}"
alt="{{ .Name }}"
class="w-full h-48 object-cover"
/>
<div class="p-4">
{{ if gt .Stock 0 }}
<div class="stock-badge bg-primary">
In Stock: {{ .Stock }}
</div>
{{ else if eq .Stock 0 }}
<div class="stock-badge bg-red-500">Out of Stock</div>
{{ else }}
<div
class="stock-badge bg-secondary-primary dark:bg-secondary-dark-primary"
>
Unlimited
</div>
{{ end }}
<div class="h-48 flex items-center justify-center">
<img
src="{{ .Image }}"
alt="{{ .Name }}"
class="object-contain h-full w-full"
/>
</div>
<div class="p-4 flex flex-col flex-grow">
<h2 class="text-xl font-semibold mb-2">
{{ .Name }}
</h2>
<p
class="text-text-secondary dark:text-text-dark-secondary mb-4"
class="text-text-secondary dark:text-text-dark-secondary mb-4 flex-grow"
>
{{ .Description }}
</p>
<div class="flex justify-between items-center">
<div
class="flex justify-between items-center mt-auto"
>
<p
class="text-lg font-bold text-primary dark:text-primary-dark"
>
🍂 {{ .Price }}
</p>
<a
href="/shop/buy/{{ .ID }}"
class="bg-primary hover:bg-primary-dark text-white font-bold py-2 px-4 rounded"
class="bg-primary hover:bg-primary-dark text-white font-bold py-2 px-4 rounded {{ if eq .Stock 0 }}cursor-not-allowed opacity-50{{ end }}"
{{
if
eq
.Stock
0
}}disabled{{
end
}}
>
Buy Now
{{ if eq .Stock 0 }}Out of Stock{{ else
}}Buy Now{{ end }}
</a>
</div>
</div>
Expand Down

0 comments on commit 820d218

Please sign in to comment.