(FEAT): Updated recipe repo to include recipe favorite status.
This means we need to pass the user id into the various methods that call it. But, since it is a pointer, we can use nil if we don't have a user to check with (this is noted in the service).
This commit is contained in:
parent
bebeb25492
commit
79bee1cde7
@ -104,15 +104,22 @@ func RecipePage(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get signed in user, if they exist
|
||||
var userId *int = nil
|
||||
if domainServer.IsLoggedIn(ctx) {
|
||||
storeId := ctx.MustGet("userId").(int)
|
||||
userId = &storeId
|
||||
}
|
||||
|
||||
// Get recipe
|
||||
recipe, err := deps.RecipeService.GetRecipe(parsed)
|
||||
recipe, err := deps.RecipeService.GetRecipe(parsed, userId)
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: %s\n", err.Error())
|
||||
ctx.JSON(400, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Get user
|
||||
// Get user (owner)
|
||||
user, err := deps.UserService.GetUser(recipe.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: %s\n", err.Error())
|
||||
|
||||
@ -29,7 +29,7 @@ func NewEngagementService(engagementRepository domain.EngagementRepository, reci
|
||||
// A message will be generated using the recipe data and then used to add a view engagement to the
|
||||
// database.
|
||||
func (s *EngagementService) UserViewRecipe(userId, recipeId int) (domain.Engagement, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId)
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
@ -43,17 +43,17 @@ func (s *EngagementService) UserViewRecipe(userId, recipeId int) (domain.Engagem
|
||||
// A message will be generated using the recipe data and then used to add a like engagement to the
|
||||
// database.
|
||||
func (s *EngagementService) UserFavoriteRecipe(userId, recipeId int) (domain.Engagement, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId)
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
// Update the favorites DB
|
||||
// Update the favorites DB
|
||||
liked, err := s.engagementRepository.UserFavoriteRecipeToggle(userId, recipeId)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
|
||||
// Determine if this like is a saving or unsaving
|
||||
var message string
|
||||
if liked {
|
||||
@ -70,7 +70,7 @@ func (s *EngagementService) UserFavoriteRecipe(userId, recipeId int) (domain.Eng
|
||||
// A message will be generated using the recipe data and then used to add a make engagement to the
|
||||
// database.
|
||||
func (s *EngagementService) UserMakeRecipe(userId, recipeId int) (domain.Engagement, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId)
|
||||
recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId)
|
||||
if err != nil {
|
||||
return domain.Engagement{}, err
|
||||
}
|
||||
|
||||
@ -123,8 +123,8 @@ func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
|
||||
// GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore,
|
||||
// if the recipe is nil, an error will be returned, so the caller does not need to check for a nil
|
||||
// recipe (e.g., if the error is nil the recipe exists)
|
||||
func (s *RecipeService) GetRecipe(id int) (*domain.Recipe, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(id)
|
||||
func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
||||
recipe, err := s.recipeRepository.GetRecipe(id, userId)
|
||||
|
||||
if recipe == nil {
|
||||
return nil, fmt.Errorf("Failed to get recipe from database. Nil result.")
|
||||
|
||||
@ -55,7 +55,8 @@ type RecipeIngredient struct {
|
||||
|
||||
// Recipe is the database model of a recipe. There is no need to map to a different model so
|
||||
// this will remain in the domain. The Tags field should be loaded from the external Tags table,
|
||||
// but is still attached to this domain object.
|
||||
// but is still attached to this domain object. The Favorite field should also be loaded from
|
||||
// the external favorites table, these are user specific.
|
||||
type Recipe struct {
|
||||
Id int
|
||||
Title string
|
||||
@ -70,6 +71,7 @@ type Recipe struct {
|
||||
Modified *time.Time // Pointer to allow null
|
||||
Created time.Time
|
||||
Tags []Tag
|
||||
Favorite bool // Per requesting user
|
||||
}
|
||||
|
||||
// SearchFilters is a model which represents the required filters to complete a recipe search.
|
||||
|
||||
@ -2,9 +2,10 @@ package domain
|
||||
|
||||
type RecipeRepository interface {
|
||||
CreateRecipe(recipe *Recipe) error
|
||||
GetRecipe(id int) (*Recipe, error)
|
||||
GetRecipe(id int, userId *int) (*Recipe, error)
|
||||
SearchRecipes(filters SearchFilters) ([]Recipe, error)
|
||||
CreateRecipeTags(recipe Recipe, tags []string) error
|
||||
GetUserRecipes(id int) ([]Recipe, error)
|
||||
GetRecipeTags(recipe *Recipe) error
|
||||
GetRecipeFavorite(recipe *Recipe, userId int) error
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import "github.com/gin-gonic/gin"
|
||||
|
||||
type RecipeService interface {
|
||||
CreateRecipe(ctx *gin.Context) (*Recipe, error)
|
||||
GetRecipe(id int) (*Recipe, error)
|
||||
GetRecipe(id int, userId *int) (*Recipe, error)
|
||||
SearchRecipes(filters SearchFilters) ([]Recipe, error)
|
||||
GetUserRecipes(id int) ([]Recipe, error)
|
||||
}
|
||||
|
||||
@ -92,7 +92,7 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
||||
// GetRecipe gets a recipe from the database via its ID. The operation is wrapped in a transaction
|
||||
// for added safety. The repository will not check for a nil result, instead the service will. Callers
|
||||
// are responsible for protecting against double nil results. Any errors will be bubbled to the caller.
|
||||
func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) {
|
||||
func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
@ -153,7 +153,18 @@ func (r *RecipeRepository) GetRecipe(id int) (*domain.Recipe, error) {
|
||||
}
|
||||
|
||||
// Add tags
|
||||
r.GetRecipeTags(&recipe)
|
||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
||||
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
||||
}
|
||||
|
||||
// Get favorite status, if user id is provided
|
||||
if userId != nil {
|
||||
if err := r.GetRecipeFavorite(&recipe, *userId); err != nil {
|
||||
fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error())
|
||||
}
|
||||
} else {
|
||||
recipe.Favorite = false
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
@ -368,7 +379,9 @@ func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters) ([]domain
|
||||
}
|
||||
|
||||
// Add tags
|
||||
r.GetRecipeTags(&recipe)
|
||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
||||
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
||||
}
|
||||
|
||||
recipes = append(recipes, recipe)
|
||||
}
|
||||
@ -517,7 +530,14 @@ func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) {
|
||||
}
|
||||
|
||||
// Add tags
|
||||
r.GetRecipeTags(&recipe)
|
||||
if err := r.GetRecipeTags(&recipe); err != nil {
|
||||
fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
|
||||
}
|
||||
|
||||
// Get favorite status
|
||||
if err := r.GetRecipeFavorite(&recipe, id); err != nil {
|
||||
fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error())
|
||||
}
|
||||
|
||||
recipes = append(recipes, recipe)
|
||||
}
|
||||
@ -571,3 +591,30 @@ func (r *RecipeRepository) GetRecipeTags(recipe *domain.Recipe) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRecipeFavorite requires a recipe to be filled with at least an ID. This function will use the
|
||||
// ID defined in the provided recipe to fill the favorite status of the recipe, based on the provided
|
||||
// userId. The recipe is modified in place and is not returned. Any errors will be bubbled to the caller.
|
||||
func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT COUNT(*)
|
||||
FROM favorites
|
||||
WHERE recipeid = $1 AND userid = $2;
|
||||
`
|
||||
|
||||
var count int
|
||||
if err := tx.QueryRow(query, recipe.Id, userId).Scan(&count); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("Failed to get recipe favorite. %s", err.Error())
|
||||
}
|
||||
|
||||
recipe.Favorite = count > 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -186,10 +186,10 @@ templ tagListItem(content string) {
|
||||
templ favoriteButton(favorited bool, id int) {
|
||||
if favorited {
|
||||
<button
|
||||
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_LIKE, id) }
|
||||
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id) }
|
||||
hx-trigger="click"
|
||||
hx-swap="none"
|
||||
class="flex items-center justify-center gap-x-1 rounded-lg border border-gray-300 text-gray-800 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300"
|
||||
class="flex items-center justify-center gap-x-1 rounded-lg border border-blue-300 bg-blue-50 text-gray-800 px-6 py-3 flex-grow hover:bg-blue-100 hover:border-blue-500 duration-300"
|
||||
>
|
||||
<svg class="h-6 text-red-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
@ -201,7 +201,7 @@ templ favoriteButton(favorited bool, id int) {
|
||||
</button>
|
||||
} else {
|
||||
<button
|
||||
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_LIKE, id) }
|
||||
hx-post={ fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id) }
|
||||
hx-trigger="click"
|
||||
hx-swap="none"
|
||||
class="flex items-center justify-center gap-x-1 rounded-lg border border-gray-300 text-gray-800 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300"
|
||||
@ -289,7 +289,7 @@ templ RecipePage(recipe domain.Recipe, user domainUser.User) {
|
||||
<p class="text-sm mb-2 text-gray-700">Category: { recipe.Category }</p>
|
||||
</div>
|
||||
@metadataSection(recipe)
|
||||
@buttonSection(false, recipe.Id)
|
||||
@buttonSection(recipe.Favorite, recipe.Id)
|
||||
<div class="px-4 py-8 md:px-8">
|
||||
<h3 class="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3>
|
||||
<p class="text-gray-700">{ recipe.Description }</p>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.865
|
||||
package templates
|
||||
@ -586,13 +586,13 @@ func favoriteButton(favorited bool, id int) templ.Component {
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 189, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 189, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"flex items-center justify-center gap-x-1 rounded-lg border border-gray-300 text-gray-800 px-6 py-3 flex-grow hover:bg-gray-50 hover:border-blue-300 duration-300\"><svg class=\"h-6 text-red-500\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2 9.1371C2 14 6.01943 16.5914 8.96173 18.9109C10 19.7294 11 20.5 12 20.5C13 20.5 14 19.7294 15.0383 18.9109C17.9806 16.5914 22 14 22 9.1371C22 4.27416 16.4998 0.825464 12 5.50063C7.50016 0.825464 2 4.27416 2 9.1371Z\" fill=\"currentColor\"></path></svg> Unfavorite</button>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" hx-trigger=\"click\" hx-swap=\"none\" class=\"flex items-center justify-center gap-x-1 rounded-lg border border-blue-300 bg-blue-50 text-gray-800 px-6 py-3 flex-grow hover:bg-blue-100 hover:border-blue-500 duration-300\"><svg class=\"h-6 text-red-500\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2 9.1371C2 14 6.01943 16.5914 8.96173 18.9109C10 19.7294 11 20.5 12 20.5C13 20.5 14 19.7294 15.0383 18.9109C17.9806 16.5914 22 14 22 9.1371C22 4.27416 16.4998 0.825464 12 5.50063C7.50016 0.825464 2 4.27416 2 9.1371Z\" fill=\"currentColor\"></path></svg> Unfavorite</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -604,7 +604,7 @@ func favoriteButton(favorited bool, id int) templ.Component {
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(domainServer.API_ENGAGEMENT_FAVORITE, id))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 204, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/pages/recipe.templ`, Line: 204, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@ -807,7 +807,7 @@ func RecipePage(recipe domain.Recipe, user domainUser.User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = buttonSection(false, recipe.Id).Render(ctx, templ_7745c5c3_Buffer)
|
||||
templ_7745c5c3_Err = buttonSection(recipe.Favorite, recipe.Id).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
monospace;
|
||||
--color-red-100: oklch(93.6% 0.032 17.717);
|
||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||
--color-red-600: oklch(57.7% 0.245 27.325);
|
||||
--color-green-300: oklch(87.1% 0.15 154.449);
|
||||
--color-green-500: oklch(72.3% 0.219 149.579);
|
||||
--color-blue-50: oklch(97% 0.014 254.604);
|
||||
@ -240,9 +239,6 @@
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
.top-1 {
|
||||
top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.top-1\/2 {
|
||||
top: calc(1/2 * 100%);
|
||||
}
|
||||
@ -252,9 +248,6 @@
|
||||
.left-0 {
|
||||
left: calc(var(--spacing) * 0);
|
||||
}
|
||||
.left-1 {
|
||||
left: calc(var(--spacing) * 1);
|
||||
}
|
||||
.left-1\/2 {
|
||||
left: calc(1/2 * 100%);
|
||||
}
|
||||
@ -426,18 +419,12 @@
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.w-1 {
|
||||
width: calc(var(--spacing) * 1);
|
||||
}
|
||||
.w-1\/3 {
|
||||
width: calc(1/3 * 100%);
|
||||
}
|
||||
.w-1\/4 {
|
||||
width: calc(1/4 * 100%);
|
||||
}
|
||||
.w-3 {
|
||||
width: calc(var(--spacing) * 3);
|
||||
}
|
||||
.w-3\/4 {
|
||||
width: calc(3/4 * 100%);
|
||||
}
|
||||
@ -450,9 +437,6 @@
|
||||
.w-5 {
|
||||
width: calc(var(--spacing) * 5);
|
||||
}
|
||||
.w-9 {
|
||||
width: calc(var(--spacing) * 9);
|
||||
}
|
||||
.w-9\/10 {
|
||||
width: calc(9/10 * 100%);
|
||||
}
|
||||
@ -471,9 +455,6 @@
|
||||
.max-w-2xl {
|
||||
max-width: var(--container-2xl);
|
||||
}
|
||||
.flex-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@ -483,21 +464,10 @@
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.-translate-x-1 {
|
||||
--tw-translate-x: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-x-1\/2 {
|
||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1 {
|
||||
--tw-translate-y: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1\/2 {
|
||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
@ -510,15 +480,9 @@
|
||||
--tw-scale-y: 50%;
|
||||
scale: var(--tw-scale-x) var(--tw-scale-y);
|
||||
}
|
||||
.transform {
|
||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||
}
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.resize {
|
||||
resize: both;
|
||||
}
|
||||
.resize-none {
|
||||
resize: none;
|
||||
}
|
||||
@ -657,9 +621,6 @@
|
||||
.border-gray-300 {
|
||||
border-color: var(--color-gray-300);
|
||||
}
|
||||
.border-green-300 {
|
||||
border-color: var(--color-green-300);
|
||||
}
|
||||
.border-green-500 {
|
||||
border-color: var(--color-green-500);
|
||||
}
|
||||
@ -669,9 +630,6 @@
|
||||
.border-white {
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
.bg-\[\#f8f8f8\] {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
.bg-black {
|
||||
background-color: var(--color-black);
|
||||
}
|
||||
@ -1132,6 +1090,20 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:border-blue-500 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
border-color: var(--color-blue-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-blue-100 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: var(--color-blue-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-blue-200 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@ -1537,26 +1509,6 @@
|
||||
inherits: false;
|
||||
initial-value: 1;
|
||||
}
|
||||
@property --tw-rotate-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-rotate-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-rotate-z {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-skew-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-skew-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-border-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
@ -1766,11 +1718,6 @@
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-scale-z: 1;
|
||||
--tw-rotate-x: initial;
|
||||
--tw-rotate-y: initial;
|
||||
--tw-rotate-z: initial;
|
||||
--tw-skew-x: initial;
|
||||
--tw-skew-y: initial;
|
||||
--tw-border-style: solid;
|
||||
--tw-gradient-position: initial;
|
||||
--tw-gradient-from: #0000;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user