Compare commits
No commits in common. "ea93bb7ab875b39e54b9e15df530a2730df5d6f0" and "695183bc9959e0ef6402a077a94be0b9c8f9a4ea" have entirely different histories.
ea93bb7ab8
...
695183bc99
@ -1,5 +1 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
- [x] Recipes: Represents a single recipe.
|
- [ ] Recipes: Represents a single recipe.
|
||||||
- [x] ID (PK) Serial
|
- [ ] ID (PK) Serial
|
||||||
- [x] Title (Required) string(128)
|
- [ ] Title (Unique, Required) string(128)
|
||||||
- [x] Description (Required) text
|
- [ ] Description (Required) text
|
||||||
- [x] Instructions (Required) string(1024)[]
|
- [ ] Instructions (Required) string(1024)[]
|
||||||
- [x] Serves (Required) int(0..16)
|
- [ ] Serves (Required) int(0..16)
|
||||||
- [x] Difficulty (Required) int(1..5)
|
- [ ] Difficulty (Required) int(1..5)
|
||||||
- [x] Duration (Required) JSONB({ "total": int, "prep": int, "cook": int })
|
- [ ] Duration (Required) JSONB({ "total": int, "prep": int, "cook": int })
|
||||||
- [x] Category (Required) E_Meal (defined in the [enums](#enums-and-types) section)
|
- [ ] Category (Required) E_Meal (defined in the [enums](#enums-and-types) section)
|
||||||
- [x] Ingredients (Required) JSONB({ "item_a": [{ "name": string, "quantity": string }], "item_b": ... })
|
- [ ] Ingredients (Required) JSONB({ "item_a": [{ "name": string, "quantity": string }], "item_b": ... })
|
||||||
- [x] UserId (FK: User.Id) Serial
|
- [ ] UserId (FK: User.Id) Serial
|
||||||
- [x] Modified () date/time stamp
|
- [ ] Modified () date/time stamp
|
||||||
- [x] Created (Required) date/time stamp
|
- [ ] 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.
|
||||||
|
|
||||||
- [x] E_Meal: Type to represent the type of meal of a recipe.
|
- [ ] E_Meal: Type to represent the type of meal of a recipe.
|
||||||
- [x] breakfast: string
|
- [ ] breakfast: string
|
||||||
- [x] lunch: string
|
- [ ] lunch: string
|
||||||
- [x] dinner: string
|
- [ ] dinner: string
|
||||||
- [x] dessert: string
|
- [ ] desert: string
|
||||||
- [x] snack: string
|
- [ ] snack: string
|
||||||
- [x] side: string
|
- [ ] side: string
|
||||||
- [x] other: string
|
- [ ] 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
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
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})
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
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,15 +113,12 @@ 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
|
||||||
@ -137,7 +134,6 @@ 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")
|
||||||
@ -165,18 +161,10 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package domain
|
|
||||||
|
|
||||||
type RecipeRepository interface {
|
|
||||||
// TODO: Not sure the input type yet
|
|
||||||
CreateRecipe(recipe *Recipe) error
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
package domain
|
|
||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
|
||||||
|
|
||||||
type RecipeService interface {
|
|
||||||
CreateRecipe(ctx *gin.Context) Recipe
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -4,14 +4,12 @@ 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 {
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
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,7 +56,6 @@ 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,10 +131,6 @@ 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,282 +3,5 @@ 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,6 +8,8 @@
|
|||||||
--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);
|
||||||
@ -17,7 +19,6 @@
|
|||||||
--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);
|
||||||
@ -34,8 +35,6 @@
|
|||||||
--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;
|
||||||
@ -206,6 +205,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
.pointer-events-none {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.absolute {
|
.absolute {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
@ -215,6 +217,9 @@
|
|||||||
.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%);
|
||||||
}
|
}
|
||||||
@ -224,6 +229,9 @@
|
|||||||
.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%);
|
||||||
}
|
}
|
||||||
@ -242,6 +250,9 @@
|
|||||||
.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);
|
||||||
}
|
}
|
||||||
@ -251,6 +262,9 @@
|
|||||||
.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);
|
||||||
}
|
}
|
||||||
@ -266,18 +280,15 @@
|
|||||||
.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);
|
||||||
}
|
}
|
||||||
@ -341,6 +352,9 @@
|
|||||||
.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%);
|
||||||
}
|
}
|
||||||
@ -353,12 +367,21 @@
|
|||||||
.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);
|
||||||
}
|
}
|
||||||
@ -374,16 +397,27 @@
|
|||||||
.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;
|
||||||
}
|
}
|
||||||
.flex-grow {
|
.border-collapse {
|
||||||
flex-grow: 1;
|
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 {
|
.-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);
|
||||||
@ -391,8 +425,8 @@
|
|||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.resize-none {
|
.resize {
|
||||||
resize: none;
|
resize: both;
|
||||||
}
|
}
|
||||||
.flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -424,9 +458,6 @@
|
|||||||
.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);
|
||||||
}
|
}
|
||||||
@ -436,6 +467,9 @@
|
|||||||
.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;
|
||||||
}
|
}
|
||||||
@ -458,6 +492,10 @@
|
|||||||
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;
|
||||||
@ -491,18 +529,15 @@
|
|||||||
.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);
|
||||||
}
|
}
|
||||||
@ -524,10 +559,6 @@
|
|||||||
--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));
|
||||||
@ -540,16 +571,15 @@
|
|||||||
--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);
|
||||||
}
|
}
|
||||||
@ -559,9 +589,6 @@
|
|||||||
.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);
|
||||||
}
|
}
|
||||||
@ -606,10 +633,6 @@
|
|||||||
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));
|
||||||
@ -671,6 +694,9 @@
|
|||||||
.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);
|
||||||
}
|
}
|
||||||
@ -680,6 +706,9 @@
|
|||||||
.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);
|
||||||
@ -758,48 +787,6 @@
|
|||||||
.\[-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) {
|
||||||
@ -814,13 +801,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.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) {
|
||||||
@ -835,6 +815,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.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) {
|
||||||
@ -943,11 +930,6 @@
|
|||||||
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;
|
||||||
@ -1044,6 +1026,12 @@
|
|||||||
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