Compare commits
2 Commits
695183bc99
...
ea93bb7ab8
| Author | SHA1 | Date | |
|---|---|---|---|
| ea93bb7ab8 | |||
|
|
b17c5774e9 |
@ -1 +1,5 @@
|
|||||||
# Potion: Recipe Sharing Platform
|
# Potion: Recipe Sharing Platform
|
||||||
|
|
||||||
|
## Todo List
|
||||||
|
|
||||||
|
- [-] Ingrident lists/sections
|
||||||
|
|||||||
@ -239,19 +239,19 @@ also have a list of attributes which are to be implemented at the database level
|
|||||||
data fields will also have a small example object. A more in-depth data structure can be
|
data fields will also have a small example object. A more in-depth data structure can be
|
||||||
found in **OTHER** section.
|
found in **OTHER** section.
|
||||||
|
|
||||||
- [ ] Recipes: Represents a single recipe.
|
- [x] Recipes: Represents a single recipe.
|
||||||
- [ ] ID (PK) Serial
|
- [x] ID (PK) Serial
|
||||||
- [ ] Title (Unique, Required) string(128)
|
- [x] Title (Required) string(128)
|
||||||
- [ ] Description (Required) text
|
- [x] Description (Required) text
|
||||||
- [ ] Instructions (Required) string(1024)[]
|
- [x] Instructions (Required) string(1024)[]
|
||||||
- [ ] Serves (Required) int(0..16)
|
- [x] Serves (Required) int(0..16)
|
||||||
- [ ] Difficulty (Required) int(1..5)
|
- [x] Difficulty (Required) int(1..5)
|
||||||
- [ ] Duration (Required) JSONB({ "total": int, "prep": int, "cook": int })
|
- [x] Duration (Required) JSONB({ "total": int, "prep": int, "cook": int })
|
||||||
- [ ] Category (Required) E_Meal (defined in the [enums](#enums-and-types) section)
|
- [x] Category (Required) E_Meal (defined in the [enums](#enums-and-types) section)
|
||||||
- [ ] Ingredients (Required) JSONB({ "item_a": [{ "name": string, "quantity": string }], "item_b": ... })
|
- [x] Ingredients (Required) JSONB({ "item_a": [{ "name": string, "quantity": string }], "item_b": ... })
|
||||||
- [ ] UserId (FK: User.Id) Serial
|
- [x] UserId (FK: User.Id) Serial
|
||||||
- [ ] Modified () date/time stamp
|
- [x] Modified () date/time stamp
|
||||||
- [ ] Created (Required) date/time stamp
|
- [x] Created (Required) date/time stamp
|
||||||
|
|
||||||
- [x] Users: Represents a single user.
|
- [x] Users: Represents a single user.
|
||||||
- [x] ID (PK) Serial
|
- [x] ID (PK) Serial
|
||||||
@ -325,14 +325,14 @@ found in **OTHER** section.
|
|||||||
Below is a breakdown of the required enumerated types that should be stored in the database.
|
Below is a breakdown of the required enumerated types that should be stored in the database.
|
||||||
Various tables will reference these types.
|
Various tables will reference these types.
|
||||||
|
|
||||||
- [ ] E_Meal: Type to represent the type of meal of a recipe.
|
- [x] E_Meal: Type to represent the type of meal of a recipe.
|
||||||
- [ ] breakfast: string
|
- [x] breakfast: string
|
||||||
- [ ] lunch: string
|
- [x] lunch: string
|
||||||
- [ ] dinner: string
|
- [x] dinner: string
|
||||||
- [ ] desert: string
|
- [x] dessert: string
|
||||||
- [ ] snack: string
|
- [x] snack: string
|
||||||
- [ ] side: string
|
- [x] side: string
|
||||||
- [ ] other: string
|
- [x] other: string
|
||||||
|
|
||||||
- [ ] E_Notification: Type to represent a type of user notification.
|
- [ ] E_Notification: Type to represent a type of user notification.
|
||||||
- [ ] comment: string
|
- [ ] comment: string
|
||||||
|
|||||||
49
internal/app/handlers/recipe_handler.go
Normal file
49
internal/app/handlers/recipe_handler.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
// domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateRecipe(ctx *gin.Context) {
|
||||||
|
// deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||||
|
|
||||||
|
title := ctx.PostForm("title")
|
||||||
|
description := ctx.PostForm("description")
|
||||||
|
preparation := ctx.PostForm("preparation-time")
|
||||||
|
cook := ctx.PostForm("cook-time")
|
||||||
|
serving := ctx.PostForm("serving-size")
|
||||||
|
category := ctx.PostForm("category")
|
||||||
|
difficulty := ctx.PostForm("difficulty")
|
||||||
|
ingredients := ctx.PostFormArray("ingredients")
|
||||||
|
quantity := ctx.PostFormArray("quantity")
|
||||||
|
instructions := ctx.PostFormArray("instructions")
|
||||||
|
tags := ctx.PostForm("tags") // this is a list of strings split with a comma (,)
|
||||||
|
|
||||||
|
// Have to get the image differently
|
||||||
|
image, err := ctx.FormFile("image")
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"cook time": cook,
|
||||||
|
"preparation time": preparation,
|
||||||
|
"serving size": serving,
|
||||||
|
"category": category,
|
||||||
|
"difficulty": difficulty,
|
||||||
|
"ingredients": ingredients,
|
||||||
|
"quantity": quantity,
|
||||||
|
"instructions": instructions,
|
||||||
|
"tags": tags,
|
||||||
|
"image": image.Filename,
|
||||||
|
})
|
||||||
|
|
||||||
|
// deps.RecipeService.CreateRecipe(ctx)
|
||||||
|
// ctx.JSON(http.StatusCreated, gin.H{"recipe": recipe})
|
||||||
|
}
|
||||||
75
internal/app/handlers/state_handler.go
Normal file
75
internal/app/handlers/state_handler.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TAG_HTML = `
|
||||||
|
<li
|
||||||
|
hx-post="/v1/web/state/tags/delete"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#tag-list"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-include="#tag-list"
|
||||||
|
hx-vals='{"target": "%s"}'
|
||||||
|
class="flex text-xs items-center bg-blue-100 w-fit px-2 py-1 rounded-full gap-x-1 select-none cursor-pointer hover:bg-blue-200 duration-300">
|
||||||
|
× %s
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
|
||||||
|
const TAG_LIST_HTML = `
|
||||||
|
<input
|
||||||
|
hx-swap-oob="outerHTML"
|
||||||
|
type="hidden"
|
||||||
|
name="tags"
|
||||||
|
id="tags"
|
||||||
|
value="%s"
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
|
||||||
|
func NewTag(ctx *gin.Context) {
|
||||||
|
tag := strings.ToLower(ctx.PostForm("tag"))
|
||||||
|
tags := strings.Split(ctx.PostForm("tags"), ",")
|
||||||
|
|
||||||
|
tags = append([]string{tag}, tags...)
|
||||||
|
|
||||||
|
var html string
|
||||||
|
var cleaned_tags []string
|
||||||
|
for _, tag := range tags {
|
||||||
|
if tag != "" {
|
||||||
|
html += fmt.Sprintf(TAG_HTML, tag, tag)
|
||||||
|
|
||||||
|
// Ensure that the list provided does not contain blank spaces.
|
||||||
|
// This is another measure to ensure this state is bulletproof.
|
||||||
|
cleaned_tags = append(cleaned_tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute OOB swap for the tags
|
||||||
|
html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(cleaned_tags, ","))
|
||||||
|
|
||||||
|
ctx.String(http.StatusOK, html)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteTag(ctx *gin.Context) {
|
||||||
|
tags := strings.Split(ctx.PostForm("tags"), ",")
|
||||||
|
target := ctx.PostForm("target")
|
||||||
|
|
||||||
|
var html string
|
||||||
|
var new_tags []string
|
||||||
|
for _, tag := range tags {
|
||||||
|
if tag != target && tag != "" {
|
||||||
|
html += fmt.Sprintf(TAG_HTML, tag, tag)
|
||||||
|
new_tags = append(new_tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute OOB swap for the tags
|
||||||
|
html += fmt.Sprintf(TAG_LIST_HTML, strings.Join(new_tags, ","))
|
||||||
|
|
||||||
|
ctx.String(http.StatusOK, html)
|
||||||
|
}
|
||||||
@ -113,12 +113,15 @@ func (s *Server) Setup() *Server {
|
|||||||
|
|
||||||
// Initialize and inject dependencies
|
// Initialize and inject dependencies
|
||||||
userRepo := repository.NewUserRepository(s.DB)
|
userRepo := repository.NewUserRepository(s.DB)
|
||||||
|
recipeRepo := repository.NewRecipeRepository(s.DB)
|
||||||
userService := service.NewUserService(userRepo)
|
userService := service.NewUserService(userRepo)
|
||||||
authService := service.NewAuthService(userRepo, jwtSecret)
|
authService := service.NewAuthService(userRepo, jwtSecret)
|
||||||
|
recipeService := service.NewRecipeService(recipeRepo)
|
||||||
|
|
||||||
deps := &domain.InjectedDependencies{
|
deps := &domain.InjectedDependencies{
|
||||||
UserService: userService,
|
UserService: userService,
|
||||||
AuthService: authService,
|
AuthService: authService,
|
||||||
|
RecipeService: recipeService,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply middleware
|
// Apply middleware
|
||||||
@ -134,6 +137,7 @@ func (s *Server) Setup() *Server {
|
|||||||
// Domain specific routers
|
// Domain specific routers
|
||||||
router_web := router_v1.Group(domain.WEB)
|
router_web := router_v1.Group(domain.WEB)
|
||||||
router_api := router_v1.Group(domain.API)
|
router_api := router_v1.Group(domain.API)
|
||||||
|
router_state := router_web.Group("state")
|
||||||
|
|
||||||
// Static routes
|
// Static routes
|
||||||
router_web.Static("/static", "./web/static")
|
router_web.Static("/static", "./web/static")
|
||||||
@ -161,10 +165,18 @@ func (s *Server) Setup() *Server {
|
|||||||
router_web.GET("/profile", handlers.ProfilePage)
|
router_web.GET("/profile", handlers.ProfilePage)
|
||||||
router_web.GET("/list", handlers.ListPage)
|
router_web.GET("/list", handlers.ListPage)
|
||||||
|
|
||||||
|
// WEB state endpoints
|
||||||
|
router_state.POST("/tags", handlers.NewTag)
|
||||||
|
router_state.POST("/tags/delete", handlers.DeleteTag)
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
router_api.GET("/auth/login", handlers.GoogleLogin)
|
router_api.GET("/auth/login", handlers.GoogleLogin)
|
||||||
router_api.GET("/auth/callback", handlers.GoogleCallback)
|
router_api.GET("/auth/callback", handlers.GoogleCallback)
|
||||||
router_api.GET("/auth/logout", handlers.Logout)
|
router_api.GET("/auth/logout", handlers.Logout)
|
||||||
|
|
||||||
|
// Recipe endpoints
|
||||||
|
// TODO: This should be post. Temp!
|
||||||
|
router_api.POST("/recipe", handlers.CreateRecipe)
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
66
internal/app/service/recipe_service.go
Normal file
66
internal/app/service/recipe_service.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecipeService implements the domain.RecipeService defined in the domain module.
|
||||||
|
type RecipeService struct {
|
||||||
|
recipeRepository domain.RecipeRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time check to ensure the RecipeService implements domain.RecipeService
|
||||||
|
var _ domain.RecipeService = (*RecipeService)(nil)
|
||||||
|
|
||||||
|
// NewRecipeService creates a user service object which can be passed into the context. The service
|
||||||
|
// requires a recipe repository which it will use to hit the database when needed.
|
||||||
|
func NewRecipeService(recipeRepository domain.RecipeRepository) domain.RecipeService {
|
||||||
|
return &RecipeService{recipeRepository: recipeRepository}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RecipeService) CreateRecipe(ctx *gin.Context) domain.Recipe {
|
||||||
|
// TODO: Implement
|
||||||
|
|
||||||
|
recipe := domain.Recipe{
|
||||||
|
Title: "Delicious Go Curry",
|
||||||
|
Description: "A savory and easy-to-make curry, perfect for weeknights.",
|
||||||
|
Instructions: []string{
|
||||||
|
"Chop all vegetables.",
|
||||||
|
"Sauté onions and garlic until fragrant.",
|
||||||
|
"Add curry paste and stir for 1 minute.",
|
||||||
|
"Add coconut milk and vegetables, simmer until cooked.",
|
||||||
|
"Serve with rice.",
|
||||||
|
},
|
||||||
|
Serves: 4,
|
||||||
|
Difficulty: 3,
|
||||||
|
Duration: domain.RecipeDuration{
|
||||||
|
Total: 45,
|
||||||
|
Prep: 15,
|
||||||
|
Cook: 30,
|
||||||
|
},
|
||||||
|
Category: domain.MealDinner, // Using our EMeal type. Ensure this matches an updated enum value.
|
||||||
|
Ingredients: []domain.RecipeIngredient{
|
||||||
|
{Name: "Onion", Quantity: "1 large"},
|
||||||
|
{Name: "Garlic", Quantity: "3 cloves"},
|
||||||
|
{Name: "Curry Paste", Quantity: "2 tbsp"},
|
||||||
|
{Name: "Coconut Milk", Quantity: "400ml can"},
|
||||||
|
{Name: "Broccoli", Quantity: "1 head"},
|
||||||
|
{Name: "Bell Pepper", Quantity: "1 red"},
|
||||||
|
{Name: "Rice", Quantity: "As needed"},
|
||||||
|
},
|
||||||
|
UserId: 3,
|
||||||
|
Created: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.recipeRepository.CreateRecipe(&recipe); err != nil {
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{"err": err.Error()})
|
||||||
|
return domain.Recipe{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusCreated, gin.H{"recipe": recipe})
|
||||||
|
return recipe
|
||||||
|
}
|
||||||
49
internal/domain/recipe/recipe.go
Normal file
49
internal/domain/recipe/recipe.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// RecipeDuration is the duration to prepare recipe. It has JSON tags which allows it to be
|
||||||
|
// marshaled into a JSON object and stored in the database (JSONB).
|
||||||
|
type RecipeDuration struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Prep int `json:"prep"`
|
||||||
|
Cook int `json:"cook"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecipeMeal is the database enum E_MEAL which defines the meal type of a recipe. Postgres enums
|
||||||
|
// are case sensitive so these must match the values in the database exactly.
|
||||||
|
type RecipeMeal string
|
||||||
|
|
||||||
|
const (
|
||||||
|
MealBreakfast RecipeMeal = "breakfast"
|
||||||
|
MealLunch RecipeMeal = "lunch"
|
||||||
|
MealDinner RecipeMeal = "dinner"
|
||||||
|
MealDessert RecipeMeal = "dessert"
|
||||||
|
MealSnack RecipeMeal = "snack"
|
||||||
|
MealSide RecipeMeal = "side"
|
||||||
|
MealOther RecipeMeal = "other"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecipeIngredient is a single ingredients in a recipe. These have JSON tags which allow them
|
||||||
|
// to be marshaled into a JSON array and stored in the database (JSONB).
|
||||||
|
type RecipeIngredient struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Quantity string `json:"quantity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
type Recipe struct {
|
||||||
|
Id int
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Instructions []string
|
||||||
|
Serves int
|
||||||
|
Difficulty int
|
||||||
|
Duration RecipeDuration
|
||||||
|
Category RecipeMeal
|
||||||
|
Ingredients []RecipeIngredient // Just a list of ingredients
|
||||||
|
UserId int
|
||||||
|
Modified *time.Time // Pointer to allow null
|
||||||
|
Created time.Time
|
||||||
|
}
|
||||||
6
internal/domain/recipe/repository.go
Normal file
6
internal/domain/recipe/repository.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
type RecipeRepository interface {
|
||||||
|
// TODO: Not sure the input type yet
|
||||||
|
CreateRecipe(recipe *Recipe) error
|
||||||
|
}
|
||||||
8
internal/domain/recipe/service.go
Normal file
8
internal/domain/recipe/service.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
type RecipeService interface {
|
||||||
|
CreateRecipe(ctx *gin.Context) Recipe
|
||||||
|
}
|
||||||
|
|
||||||
@ -4,12 +4,14 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
|
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
|
||||||
|
domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||||
domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InjectedDependencies struct {
|
type InjectedDependencies struct {
|
||||||
UserService domainUser.UserService
|
UserService domainUser.UserService
|
||||||
AuthService domainAuth.AuthService
|
AuthService domainAuth.AuthService
|
||||||
|
RecipeService domainRecipe.RecipeService
|
||||||
}
|
}
|
||||||
|
|
||||||
type JwtClaims struct {
|
type JwtClaims struct {
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com)
|
||||||
|
-- Desc: Create the E_MEAL enum.
|
||||||
|
-- Date: 06/25/2025
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TYPE E_MEAL AS ENUM(
|
||||||
|
'breakfast',
|
||||||
|
'lunch',
|
||||||
|
'dinner',
|
||||||
|
'dessert',
|
||||||
|
'snack',
|
||||||
|
'side',
|
||||||
|
'other'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com)
|
||||||
|
-- Desc: Create the recipes table in the database.
|
||||||
|
-- Date: 06/25/2025
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Create the recipes table
|
||||||
|
CREATE TABLE IF NOT EXISTS Recipes (
|
||||||
|
Id SERIAL PRIMARY KEY NOT NULL,
|
||||||
|
Title VARCHAR(128) NOT NULL,
|
||||||
|
Description TEXT NOT NULL,
|
||||||
|
Instructions VARCHAR(1024)[] NOT NULL,
|
||||||
|
Serves INTEGER NOT NULL CHECK (serves >= 0 AND serves <= 16),
|
||||||
|
Difficulty INTEGER NOT NULL CHECK (difficulty >= 1 AND difficulty <= 5),
|
||||||
|
Duration JSONB NOT NULL,
|
||||||
|
Category E_MEAL NOT NULL,
|
||||||
|
Ingredients JSONB NOT NULL,
|
||||||
|
UserId INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
Modified TIMESTAMPTZ,
|
||||||
|
Created TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecipeRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time check to ensure the RecipeRepository implements domain.RecipeRepository
|
||||||
|
var _ domain.RecipeRepository = (*RecipeRepository)(nil)
|
||||||
|
|
||||||
|
// NewRecipeRepository creates a user repository object which is used by the user service to access
|
||||||
|
// the database. Any recipe related database operations will take place in this repository.
|
||||||
|
func NewRecipeRepository(db *sql.DB) domain.RecipeRepository {
|
||||||
|
return &RecipeRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This function modified the provided recipe with the new values, such as id and time stamp
|
||||||
|
func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
|
||||||
|
tx, err := r.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `INSERT INTO recipes (
|
||||||
|
title, description, instructions, serves, difficulty,
|
||||||
|
duration, category, ingredients, userid, modified, created
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
|
||||||
|
) RETURNING id;`
|
||||||
|
|
||||||
|
// NOTE: Data steps
|
||||||
|
// cast duration to JSON
|
||||||
|
// cast ingredients to JSON
|
||||||
|
// cast category to string
|
||||||
|
// use nil for the modified time
|
||||||
|
|
||||||
|
durationJSON, err := json.Marshal(recipe.Duration)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientsJSON, err := json.Marshal(recipe.Ingredients)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var id int
|
||||||
|
if err = tx.QueryRow(
|
||||||
|
query,
|
||||||
|
recipe.Title,
|
||||||
|
recipe.Description,
|
||||||
|
pq.Array(recipe.Instructions),
|
||||||
|
recipe.Serves,
|
||||||
|
recipe.Difficulty,
|
||||||
|
durationJSON,
|
||||||
|
string(recipe.Category),
|
||||||
|
ingredientsJSON,
|
||||||
|
recipe.UserId,
|
||||||
|
nil,
|
||||||
|
recipe.Created,
|
||||||
|
).Scan(&id); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new ID
|
||||||
|
recipe.Id = id
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -56,6 +56,7 @@ templ FilterDropdown() {
|
|||||||
Difficulty
|
Difficulty
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex text-xs flex-wrap gap-1">
|
<div class="flex text-xs flex-wrap gap-1">
|
||||||
|
@dropdownButton("Beginner")
|
||||||
@dropdownButton("Easy")
|
@dropdownButton("Easy")
|
||||||
@dropdownButton("Intermediate")
|
@dropdownButton("Intermediate")
|
||||||
@dropdownButton("Challegening")
|
@dropdownButton("Challegening")
|
||||||
|
|||||||
@ -131,6 +131,10 @@ func FilterDropdown() templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
templ_7745c5c3_Err = dropdownButton("Beginner").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
templ_7745c5c3_Err = dropdownButton("Easy").Render(ctx, templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = dropdownButton("Easy").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
|
|||||||
@ -3,5 +3,282 @@ package templates
|
|||||||
import "github.com/haydenhargreaves/Potion/internal/templates/components"
|
import "github.com/haydenhargreaves/Potion/internal/templates/components"
|
||||||
|
|
||||||
templ CreatePage() {
|
templ CreatePage() {
|
||||||
@components.Navbar("create")
|
@components.Navbar("create")
|
||||||
|
<div class="w-full h-fit flex justify-center">
|
||||||
|
<div class="mx-2 md:mx-0 w-full md:w-1/2 md:pt-14 h-full border-l border-r border-gray-300 bg-white">
|
||||||
|
@Page()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Page() {
|
||||||
|
@components.BannerText("Create Your Masterpiece")
|
||||||
|
<p id="output"></p>
|
||||||
|
<div class="mx-4 md:mx-16 my-8">
|
||||||
|
<p class="mb-8">
|
||||||
|
Welcome to the Recipe Creation Wizard! Simply fill in the details about your culinary creation,
|
||||||
|
including the recipe's name, a description, and other specifics like its category, duration,
|
||||||
|
and difficulty. Don't forget to dynamically add all your ingredients and instructions using
|
||||||
|
the dedicated buttons, and feel free to upload an appealing image. All required fields are
|
||||||
|
marked with an <span class="text-red-500">*</span>. Once everything looks perfect, just hit the "Create Recipe"
|
||||||
|
button to
|
||||||
|
share your masterpiece!
|
||||||
|
</p>
|
||||||
|
<form>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label for="title" class="text-sm mb-2">
|
||||||
|
Recipe Title
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onkeydown="return event.key != 'Enter';"
|
||||||
|
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
name="title"
|
||||||
|
placeholder="e.g., Classic Chicken Curry"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col my-4">
|
||||||
|
<label for="description" class="text-sm mb-2">
|
||||||
|
Description
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm"
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
rows="4"
|
||||||
|
placeholder="A brief description of your delicious recipe..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="my-4 flex flex-col gap-x-2">
|
||||||
|
<div class="flex flex-col flex-grow">
|
||||||
|
<label for="tags" class="text-sm mb-2">
|
||||||
|
Recipe Tags
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onkeydown="return event.key != 'Enter';"
|
||||||
|
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||||
|
hx-post="/v1/web/state/tags"
|
||||||
|
maxlength="32"
|
||||||
|
hx-trigger="keyup[keyCode==13]"
|
||||||
|
hx-on::after-request="this.value=''"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-target="#tag-list"
|
||||||
|
enterkeyhint="done"
|
||||||
|
type="text"
|
||||||
|
id="tag"
|
||||||
|
name="tag"
|
||||||
|
placeholder="e.g., Healthy"
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="tags" id="tags" value=""/>
|
||||||
|
</div>
|
||||||
|
<ul id="tag-list" class="my-2 flex gap-1 flex-wrap"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="my-4 flex gap-x-2">
|
||||||
|
<div class="flex flex-col flex-grow w-1/3">
|
||||||
|
<label for="preparation-time" class="text-sm mb-2">
|
||||||
|
Prep Time
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onkeydown="return event.key != 'Enter';"
|
||||||
|
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||||
|
type="number"
|
||||||
|
id="preparation-time"
|
||||||
|
name="preparation-time"
|
||||||
|
placeholder="e.g., 20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-grow w-1/3">
|
||||||
|
<label for="cook-time" class="text-sm mb-2">
|
||||||
|
Cook Time
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onkeydown="return event.key != 'Enter';"
|
||||||
|
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||||
|
type="number"
|
||||||
|
id="cook-time"
|
||||||
|
name="cook-time"
|
||||||
|
placeholder="e.g., 45"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-grow w-1/3">
|
||||||
|
<label for="serving-size" class="text-sm mb-2">
|
||||||
|
Serving Size
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onkeydown="return event.key != 'Enter';"
|
||||||
|
class="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||||
|
type="number"
|
||||||
|
max="16"
|
||||||
|
min="1"
|
||||||
|
id="serving-size"
|
||||||
|
name="serving-size"
|
||||||
|
placeholder="e.g., 4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-4 flex gap-x-2">
|
||||||
|
<div class="flex flex-col flex-grow w-1/3">
|
||||||
|
<label for="category" class="text-sm mb-2">
|
||||||
|
Category
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="category"
|
||||||
|
name="category"
|
||||||
|
class="border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<option value="">Select a category</option>
|
||||||
|
<option value="breakfast">Breakfast</option>
|
||||||
|
<option value="lunch">Lunch</option>
|
||||||
|
<option value="dinner">Dinner</option>
|
||||||
|
<option value="dessert">Dessert</option>
|
||||||
|
<option value="snack">Snack</option>
|
||||||
|
<option value="side">Side</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-grow w-1/3">
|
||||||
|
<label for="difficulty" class="text-sm mb-2">
|
||||||
|
Difficulty
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="difficulty"
|
||||||
|
name="difficulty"
|
||||||
|
class="border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<option value="">Select a difficulty</option>
|
||||||
|
<option value="1">Beginner</option>
|
||||||
|
<option value="2">Easy</option>
|
||||||
|
<option value="3">Intermediate</option>
|
||||||
|
<option value="4">Challenging</option>
|
||||||
|
<option value="5">Extreme</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col my-4">
|
||||||
|
<label for="ingredients" class="text-sm">
|
||||||
|
Ingredients
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div id="ingredient-list">
|
||||||
|
<div class="w-full flex gap-x-2 py-2">
|
||||||
|
<input
|
||||||
|
onkeydown="return event.key != 'Enter';"
|
||||||
|
class="flex-grow border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||||
|
type="text"
|
||||||
|
id="ingredients"
|
||||||
|
name="ingredients"
|
||||||
|
placeholder="Ingredient name (e.g., Chicken Breast)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
onkeydown="return event.key != 'Enter';"
|
||||||
|
class="w-1/3 border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||||
|
type="text"
|
||||||
|
id="quantity"
|
||||||
|
name="quantity"
|
||||||
|
placeholder="Quantity (e.g., 1lb)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick="addIngredient();"
|
||||||
|
class="text-base md:text-lg text-white bg-blue-500 w-fit px-5 py-2 rounded-lg cursor-pointer"
|
||||||
|
>
|
||||||
|
Add Ingredient
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col my-4">
|
||||||
|
<label for="instructions" class="text-sm">
|
||||||
|
Instructions
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div id="instruction-list" class="flex flex-col">
|
||||||
|
<textarea
|
||||||
|
class="border border-gray-300 my-2 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm"
|
||||||
|
id="instructions"
|
||||||
|
name="instructions"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Step 1: Describe this step..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick="addInstruction();"
|
||||||
|
class="text-base md:text-lg text-white bg-blue-500 w-fit px-5 py-2 rounded-lg cursor-pointer"
|
||||||
|
>
|
||||||
|
Add Instruction Step
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col my-4">
|
||||||
|
<label for="image" class="text-sm">
|
||||||
|
Recipe Image
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
name="image"
|
||||||
|
id="image"
|
||||||
|
class="my-2 block w-full text-sm text-placeholder file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:bg-blue-100 file:text-blue-700 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- <form hx-post="/v1/api/recipe" hx-swap="innerHTML" hx-target="#output" hx-trigger="submit"> -->
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
hx-post="/v1/api/recipe"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-target="#output"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-encoding="multipart/form-data"
|
||||||
|
class="w-full mt-8 bg-gradient-to-r from-blue-200 to-purple-200 py-2 rounded-lg text-lg cursor-pointer shadow-md"
|
||||||
|
>
|
||||||
|
Create Recipe
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function addIngredient() {
|
||||||
|
const list = document.getElementById("ingredient-list");
|
||||||
|
const item = document.createElement("div");
|
||||||
|
item.classList.add("w-full", "flex", "gap-x-2", "py-2");
|
||||||
|
item.innerHTML = `
|
||||||
|
<input
|
||||||
|
onkeydown="return event.key != 'Enter';"
|
||||||
|
class="flex-grow border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||||
|
type="text"
|
||||||
|
id="ingredients"
|
||||||
|
name="ingredients"
|
||||||
|
placeholder="Ingredient name (e.g., Chicken Breast)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
onkeydown="return event.key != 'Enter';"
|
||||||
|
class="w-1/3 border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
|
||||||
|
type="text"
|
||||||
|
id="quantity"
|
||||||
|
name="quantity"
|
||||||
|
placeholder="Quantity (e.g., 1lb)"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
list.appendChild(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addInstruction() {
|
||||||
|
const list = document.getElementById("instruction-list");
|
||||||
|
const itemNum = list.children.length + 1;
|
||||||
|
const item = document.createElement("textarea");
|
||||||
|
item.id = "instructions";
|
||||||
|
item.name = "instructions";
|
||||||
|
item.className = "border border-gray-300 my-2 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm";
|
||||||
|
item.rows = "3";
|
||||||
|
item.placeholder = `Step ${itemNum}: Describe this step...`;
|
||||||
|
list.appendChild(item);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -8,8 +8,6 @@
|
|||||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
--color-red-100: oklch(93.6% 0.032 17.717);
|
--color-red-100: oklch(93.6% 0.032 17.717);
|
||||||
--color-red-200: oklch(88.5% 0.062 18.334);
|
|
||||||
--color-red-400: oklch(70.4% 0.191 22.216);
|
|
||||||
--color-red-500: oklch(63.7% 0.237 25.331);
|
--color-red-500: oklch(63.7% 0.237 25.331);
|
||||||
--color-blue-100: oklch(93.2% 0.032 255.585);
|
--color-blue-100: oklch(93.2% 0.032 255.585);
|
||||||
--color-blue-200: oklch(88.2% 0.059 254.128);
|
--color-blue-200: oklch(88.2% 0.059 254.128);
|
||||||
@ -19,6 +17,7 @@
|
|||||||
--color-blue-600: oklch(54.6% 0.245 262.881);
|
--color-blue-600: oklch(54.6% 0.245 262.881);
|
||||||
--color-blue-700: oklch(48.8% 0.243 264.376);
|
--color-blue-700: oklch(48.8% 0.243 264.376);
|
||||||
--color-purple-100: oklch(94.6% 0.033 307.174);
|
--color-purple-100: oklch(94.6% 0.033 307.174);
|
||||||
|
--color-purple-200: oklch(90.2% 0.063 306.703);
|
||||||
--color-gray-50: oklch(98.5% 0.002 247.839);
|
--color-gray-50: oklch(98.5% 0.002 247.839);
|
||||||
--color-gray-100: oklch(96.7% 0.003 264.542);
|
--color-gray-100: oklch(96.7% 0.003 264.542);
|
||||||
--color-gray-200: oklch(92.8% 0.006 264.531);
|
--color-gray-200: oklch(92.8% 0.006 264.531);
|
||||||
@ -35,6 +34,8 @@
|
|||||||
--text-xs--line-height: calc(1 / 0.75);
|
--text-xs--line-height: calc(1 / 0.75);
|
||||||
--text-sm: 0.875rem;
|
--text-sm: 0.875rem;
|
||||||
--text-sm--line-height: calc(1.25 / 0.875);
|
--text-sm--line-height: calc(1.25 / 0.875);
|
||||||
|
--text-base: 1rem;
|
||||||
|
--text-base--line-height: calc(1.5 / 1);
|
||||||
--text-lg: 1.125rem;
|
--text-lg: 1.125rem;
|
||||||
--text-lg--line-height: calc(1.75 / 1.125);
|
--text-lg--line-height: calc(1.75 / 1.125);
|
||||||
--text-xl: 1.25rem;
|
--text-xl: 1.25rem;
|
||||||
@ -205,9 +206,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.pointer-events-none {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.absolute {
|
.absolute {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
@ -217,9 +215,6 @@
|
|||||||
.static {
|
.static {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
.top-1 {
|
|
||||||
top: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.top-1\/2 {
|
.top-1\/2 {
|
||||||
top: calc(1/2 * 100%);
|
top: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
@ -229,9 +224,6 @@
|
|||||||
.left-0 {
|
.left-0 {
|
||||||
left: calc(var(--spacing) * 0);
|
left: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.left-1 {
|
|
||||||
left: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.left-1\/2 {
|
.left-1\/2 {
|
||||||
left: calc(1/2 * 100%);
|
left: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
@ -250,9 +242,6 @@
|
|||||||
.mx-4 {
|
.mx-4 {
|
||||||
margin-inline: calc(var(--spacing) * 4);
|
margin-inline: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
.mx-8 {
|
|
||||||
margin-inline: calc(var(--spacing) * 8);
|
|
||||||
}
|
|
||||||
.my-2 {
|
.my-2 {
|
||||||
margin-block: calc(var(--spacing) * 2);
|
margin-block: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@ -262,9 +251,6 @@
|
|||||||
.my-8 {
|
.my-8 {
|
||||||
margin-block: calc(var(--spacing) * 8);
|
margin-block: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
.my-auto {
|
|
||||||
margin-block: auto;
|
|
||||||
}
|
|
||||||
.mt-2 {
|
.mt-2 {
|
||||||
margin-top: calc(var(--spacing) * 2);
|
margin-top: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@ -280,15 +266,18 @@
|
|||||||
.mt-16 {
|
.mt-16 {
|
||||||
margin-top: calc(var(--spacing) * 16);
|
margin-top: calc(var(--spacing) * 16);
|
||||||
}
|
}
|
||||||
.mt-auto {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
.mb-1 {
|
.mb-1 {
|
||||||
margin-bottom: calc(var(--spacing) * 1);
|
margin-bottom: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.mb-6 {
|
.mb-6 {
|
||||||
margin-bottom: calc(var(--spacing) * 6);
|
margin-bottom: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.mb-8 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
.mb-10 {
|
.mb-10 {
|
||||||
margin-bottom: calc(var(--spacing) * 10);
|
margin-bottom: calc(var(--spacing) * 10);
|
||||||
}
|
}
|
||||||
@ -352,9 +341,6 @@
|
|||||||
.w-1\/3 {
|
.w-1\/3 {
|
||||||
width: calc(1/3 * 100%);
|
width: calc(1/3 * 100%);
|
||||||
}
|
}
|
||||||
.w-3 {
|
|
||||||
width: calc(var(--spacing) * 3);
|
|
||||||
}
|
|
||||||
.w-3\/4 {
|
.w-3\/4 {
|
||||||
width: calc(3/4 * 100%);
|
width: calc(3/4 * 100%);
|
||||||
}
|
}
|
||||||
@ -367,21 +353,12 @@
|
|||||||
.w-5 {
|
.w-5 {
|
||||||
width: calc(var(--spacing) * 5);
|
width: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
.w-9 {
|
|
||||||
width: calc(var(--spacing) * 9);
|
|
||||||
}
|
|
||||||
.w-9\/10 {
|
.w-9\/10 {
|
||||||
width: calc(9/10 * 100%);
|
width: calc(9/10 * 100%);
|
||||||
}
|
}
|
||||||
.w-24 {
|
.w-24 {
|
||||||
width: calc(var(--spacing) * 24);
|
width: calc(var(--spacing) * 24);
|
||||||
}
|
}
|
||||||
.w-28 {
|
|
||||||
width: calc(var(--spacing) * 28);
|
|
||||||
}
|
|
||||||
.w-32 {
|
|
||||||
width: calc(var(--spacing) * 32);
|
|
||||||
}
|
|
||||||
.w-44 {
|
.w-44 {
|
||||||
width: calc(var(--spacing) * 44);
|
width: calc(var(--spacing) * 44);
|
||||||
}
|
}
|
||||||
@ -397,27 +374,16 @@
|
|||||||
.max-w-xl {
|
.max-w-xl {
|
||||||
max-width: var(--container-xl);
|
max-width: var(--container-xl);
|
||||||
}
|
}
|
||||||
.flex-shrink {
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
.flex-shrink-0 {
|
.flex-shrink-0 {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.border-collapse {
|
.flex-grow {
|
||||||
border-collapse: collapse;
|
flex-grow: 1;
|
||||||
}
|
|
||||||
.-translate-x-1 {
|
|
||||||
--tw-translate-x: calc(var(--spacing) * -1);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
}
|
||||||
.-translate-x-1\/2 {
|
.-translate-x-1\/2 {
|
||||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
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 {
|
.-translate-y-1\/2 {
|
||||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
@ -425,8 +391,8 @@
|
|||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.resize {
|
.resize-none {
|
||||||
resize: both;
|
resize: none;
|
||||||
}
|
}
|
||||||
.flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -458,6 +424,9 @@
|
|||||||
.gap-8 {
|
.gap-8 {
|
||||||
gap: calc(var(--spacing) * 8);
|
gap: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
|
.gap-x-1 {
|
||||||
|
column-gap: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
.gap-x-2 {
|
.gap-x-2 {
|
||||||
column-gap: calc(var(--spacing) * 2);
|
column-gap: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@ -467,9 +436,6 @@
|
|||||||
.gap-x-8 {
|
.gap-x-8 {
|
||||||
column-gap: calc(var(--spacing) * 8);
|
column-gap: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
.gap-x-16 {
|
|
||||||
column-gap: calc(var(--spacing) * 16);
|
|
||||||
}
|
|
||||||
.overflow-hidden {
|
.overflow-hidden {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@ -492,10 +458,6 @@
|
|||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
.border-1 {
|
|
||||||
border-style: var(--tw-border-style);
|
|
||||||
border-width: 1px;
|
|
||||||
}
|
|
||||||
.border-2 {
|
.border-2 {
|
||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
@ -529,15 +491,18 @@
|
|||||||
.border-gray-300 {
|
.border-gray-300 {
|
||||||
border-color: var(--color-gray-300);
|
border-color: var(--color-gray-300);
|
||||||
}
|
}
|
||||||
.border-red-400 {
|
|
||||||
border-color: var(--color-red-400);
|
|
||||||
}
|
|
||||||
.border-red-500 {
|
.border-red-500 {
|
||||||
border-color: var(--color-red-500);
|
border-color: var(--color-red-500);
|
||||||
}
|
}
|
||||||
.border-white {
|
.border-white {
|
||||||
border-color: var(--color-white);
|
border-color: var(--color-white);
|
||||||
}
|
}
|
||||||
|
.bg-blue-100 {
|
||||||
|
background-color: var(--color-blue-100);
|
||||||
|
}
|
||||||
|
.bg-blue-500 {
|
||||||
|
background-color: var(--color-blue-500);
|
||||||
|
}
|
||||||
.bg-gray-100 {
|
.bg-gray-100 {
|
||||||
background-color: var(--color-gray-100);
|
background-color: var(--color-gray-100);
|
||||||
}
|
}
|
||||||
@ -559,6 +524,10 @@
|
|||||||
--tw-gradient-from: var(--color-blue-100);
|
--tw-gradient-from: var(--color-blue-100);
|
||||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||||
}
|
}
|
||||||
|
.from-blue-200 {
|
||||||
|
--tw-gradient-from: var(--color-blue-200);
|
||||||
|
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||||
|
}
|
||||||
.from-blue-400 {
|
.from-blue-400 {
|
||||||
--tw-gradient-from: var(--color-blue-400);
|
--tw-gradient-from: var(--color-blue-400);
|
||||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||||
@ -571,15 +540,16 @@
|
|||||||
--tw-gradient-to: var(--color-purple-100);
|
--tw-gradient-to: var(--color-purple-100);
|
||||||
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||||
}
|
}
|
||||||
|
.to-purple-200 {
|
||||||
|
--tw-gradient-to: var(--color-purple-200);
|
||||||
|
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||||
|
}
|
||||||
.p-2 {
|
.p-2 {
|
||||||
padding: calc(var(--spacing) * 2);
|
padding: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
.p-4 {
|
.p-4 {
|
||||||
padding: calc(var(--spacing) * 4);
|
padding: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
.p-8 {
|
|
||||||
padding: calc(var(--spacing) * 8);
|
|
||||||
}
|
|
||||||
.px-1 {
|
.px-1 {
|
||||||
padding-inline: calc(var(--spacing) * 1);
|
padding-inline: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
@ -589,6 +559,9 @@
|
|||||||
.px-4 {
|
.px-4 {
|
||||||
padding-inline: calc(var(--spacing) * 4);
|
padding-inline: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.px-5 {
|
||||||
|
padding-inline: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
.px-8 {
|
.px-8 {
|
||||||
padding-inline: calc(var(--spacing) * 8);
|
padding-inline: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
@ -633,6 +606,10 @@
|
|||||||
font-size: var(--text-3xl);
|
font-size: var(--text-3xl);
|
||||||
line-height: var(--tw-leading, var(--text-3xl--line-height));
|
line-height: var(--tw-leading, var(--text-3xl--line-height));
|
||||||
}
|
}
|
||||||
|
.text-base {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
line-height: var(--tw-leading, var(--text-base--line-height));
|
||||||
|
}
|
||||||
.text-lg {
|
.text-lg {
|
||||||
font-size: var(--text-lg);
|
font-size: var(--text-lg);
|
||||||
line-height: var(--tw-leading, var(--text-lg--line-height));
|
line-height: var(--tw-leading, var(--text-lg--line-height));
|
||||||
@ -694,9 +671,6 @@
|
|||||||
.text-gray-800 {
|
.text-gray-800 {
|
||||||
color: var(--color-gray-800);
|
color: var(--color-gray-800);
|
||||||
}
|
}
|
||||||
.text-red-400 {
|
|
||||||
color: var(--color-red-400);
|
|
||||||
}
|
|
||||||
.text-red-500 {
|
.text-red-500 {
|
||||||
color: var(--color-red-500);
|
color: var(--color-red-500);
|
||||||
}
|
}
|
||||||
@ -706,9 +680,6 @@
|
|||||||
.uppercase {
|
.uppercase {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.underline {
|
|
||||||
text-decoration-line: underline;
|
|
||||||
}
|
|
||||||
.shadow {
|
.shadow {
|
||||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
@ -787,6 +758,48 @@
|
|||||||
.\[-webkit-line-clamp\:4\] {
|
.\[-webkit-line-clamp\:4\] {
|
||||||
-webkit-line-clamp: 4;
|
-webkit-line-clamp: 4;
|
||||||
}
|
}
|
||||||
|
.file\:mr-4 {
|
||||||
|
&::file-selector-button {
|
||||||
|
margin-right: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file\:rounded-lg {
|
||||||
|
&::file-selector-button {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file\:border-0 {
|
||||||
|
&::file-selector-button {
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file\:bg-blue-100 {
|
||||||
|
&::file-selector-button {
|
||||||
|
background-color: var(--color-blue-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file\:px-4 {
|
||||||
|
&::file-selector-button {
|
||||||
|
padding-inline: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file\:py-2 {
|
||||||
|
&::file-selector-button {
|
||||||
|
padding-block: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file\:text-sm {
|
||||||
|
&::file-selector-button {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.file\:text-blue-700 {
|
||||||
|
&::file-selector-button {
|
||||||
|
color: var(--color-blue-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
.hover\:cursor-pointer {
|
.hover\:cursor-pointer {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@ -801,6 +814,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hover\:bg-blue-200 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-blue-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.hover\:bg-gray-50 {
|
.hover\:bg-gray-50 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@ -815,13 +835,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.hover\:bg-red-200 {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
background-color: var(--color-red-200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hover\:text-blue-400 {
|
.hover\:text-blue-400 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@ -930,6 +943,11 @@
|
|||||||
margin-inline: calc(var(--spacing) * 0);
|
margin-inline: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.md\:mx-16 {
|
||||||
|
@media (width >= 48rem) {
|
||||||
|
margin-inline: calc(var(--spacing) * 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
.md\:flex {
|
.md\:flex {
|
||||||
@media (width >= 48rem) {
|
@media (width >= 48rem) {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -1026,12 +1044,6 @@
|
|||||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.md\:text-xl {
|
|
||||||
@media (width >= 48rem) {
|
|
||||||
font-size: var(--text-xl);
|
|
||||||
line-height: var(--tw-leading, var(--text-xl--line-height));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.lg\:flex {
|
.lg\:flex {
|
||||||
@media (width >= 64rem) {
|
@media (width >= 64rem) {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user